@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,36 @@
|
|
|
1
|
+
// Most apps would load this from their database
|
|
2
|
+
export function getOffers(shop: string, customerID: string) {
|
|
3
|
+
const offers: Record<string, any[]> = {
|
|
4
|
+
'shop.com': [
|
|
5
|
+
{
|
|
6
|
+
id: '1',
|
|
7
|
+
title: '10% off',
|
|
8
|
+
price: 10,
|
|
9
|
+
type: 'percentage',
|
|
10
|
+
customerId: 'gid://shopify/Customer/1001',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: '2',
|
|
14
|
+
title: 'Free shipping',
|
|
15
|
+
price: 0,
|
|
16
|
+
type: 'shipping',
|
|
17
|
+
customerId: 'gid://shopify/Customer/1001',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: '3',
|
|
21
|
+
title: '5% off',
|
|
22
|
+
price: 5,
|
|
23
|
+
type: 'percentage',
|
|
24
|
+
customerId: 'gid://shopify/Customer/1001',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const allOffers = offers[shop] || [];
|
|
30
|
+
// Filter offers to include only those that match the customerId
|
|
31
|
+
const filteredOffers = allOffers.filter(
|
|
32
|
+
(offer) => offer.customerId === customerID,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return filteredOffers;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {ActionFunctionArgs, LoaderFunctionArgs} from '@remix-run/node';
|
|
2
|
+
import {json} from '@remix-run/node';
|
|
3
|
+
|
|
4
|
+
import {authenticate} from '../shopify.server';
|
|
5
|
+
import {getOffers} from '../offers.server';
|
|
6
|
+
|
|
7
|
+
// The loader responds to preflight requests from Shopify
|
|
8
|
+
export const loader = async ({request}: LoaderFunctionArgs) => {
|
|
9
|
+
await authenticate.public.customerAccount(request);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const action = async ({request}: ActionFunctionArgs) => {
|
|
13
|
+
const {cors, sessionToken} = await authenticate.public.customerAccount(request);
|
|
14
|
+
|
|
15
|
+
// Get offers for the customer
|
|
16
|
+
const offers = getOffers(sessionToken.des, sessionToken.sub);
|
|
17
|
+
return cors(json({offers}));
|
|
18
|
+
};
|
package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
|
|
2
|
+
|
|
3
|
+
const data: ReferenceEntityTemplateSchema = {
|
|
4
|
+
name: 'Customer account',
|
|
5
|
+
description:
|
|
6
|
+
'The `authenticate.public.customerAccount` function ensures that customer account extension requests are coming from Shopify, and returns helpers to respond with the correct headers.',
|
|
7
|
+
category: 'Authenticate',
|
|
8
|
+
subCategory: 'Public',
|
|
9
|
+
type: 'object',
|
|
10
|
+
isVisualComponent: false,
|
|
11
|
+
definitions: [
|
|
12
|
+
{
|
|
13
|
+
title: 'authenticate.public.customerAccount',
|
|
14
|
+
description:
|
|
15
|
+
'Authenticates requests coming from Shopify customer account extensions.',
|
|
16
|
+
type: 'AuthenticateCustomerAccount',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
defaultExample: {
|
|
20
|
+
description: 'Authenticate and return offers for the customer',
|
|
21
|
+
codeblock: {
|
|
22
|
+
title: 'Authenticate and return offers for the customer',
|
|
23
|
+
tabs: [
|
|
24
|
+
{
|
|
25
|
+
title: '/app/routes/**.ts',
|
|
26
|
+
language: 'typescript',
|
|
27
|
+
code: './authenticate.public.customerAccount.doc.example.ts',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: '/app/offers.server.ts',
|
|
31
|
+
language: 'typescript',
|
|
32
|
+
code: './authenticate.public.customerAccount.doc.example.offers.ts',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
jsDocTypeExamples: ['CustomerAccountContext'],
|
|
38
|
+
related: [
|
|
39
|
+
{
|
|
40
|
+
name: 'Session token API',
|
|
41
|
+
subtitle:
|
|
42
|
+
'Customer account UI extensions API for interacting with session tokens.',
|
|
43
|
+
url: '/docs/api/customer-account-ui-extensions/latest/apis/session-token',
|
|
44
|
+
type: 'shopify',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default data;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {BasicParams} from '../../../types';
|
|
2
|
+
import {authenticateExtensionFactory} from '../extension/authenticate';
|
|
3
|
+
|
|
4
|
+
import {AuthenticateCustomerAccount} from './types';
|
|
5
|
+
|
|
6
|
+
export function authenticateCustomerAccountFactory(
|
|
7
|
+
params: BasicParams,
|
|
8
|
+
): AuthenticateCustomerAccount {
|
|
9
|
+
return authenticateExtensionFactory(
|
|
10
|
+
params,
|
|
11
|
+
'customer account',
|
|
12
|
+
) as AuthenticateCustomerAccount;
|
|
13
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {JwtPayload} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import {EnsureCORSFunction} from '../../helpers/ensure-cors-headers';
|
|
4
|
+
|
|
5
|
+
export type AuthenticateCustomerAccount = (
|
|
6
|
+
request: Request,
|
|
7
|
+
options?: AuthenticateCustomerAccountOptions,
|
|
8
|
+
) => Promise<CustomerAccountContext>;
|
|
9
|
+
|
|
10
|
+
export interface AuthenticateCustomerAccountOptions {
|
|
11
|
+
corsHeaders?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Authenticated Context for a customer account extension request
|
|
16
|
+
*/
|
|
17
|
+
export interface CustomerAccountContext {
|
|
18
|
+
/**
|
|
19
|
+
* The decoded and validated session token for the request
|
|
20
|
+
*
|
|
21
|
+
* Refer to the OAuth docs for the [session token payload](https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload).
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <caption>Using the decoded session token.</caption>
|
|
25
|
+
* <description>Get store-specific data using the `sessionToken` object.</description>
|
|
26
|
+
* ```ts
|
|
27
|
+
* // app/routes/public/my-route.ts
|
|
28
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
29
|
+
* import { authenticate } from "../shopify.server";
|
|
30
|
+
* import { getMyAppData } from "~/db/model.server";
|
|
31
|
+
*
|
|
32
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
33
|
+
* const { sessionToken } = await authenticate.public.customerAccount(
|
|
34
|
+
* request
|
|
35
|
+
* );
|
|
36
|
+
* return json(await getMyAppData({shop: sessionToken.dest}));
|
|
37
|
+
* };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
sessionToken: JwtPayload;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A function that ensures the CORS headers are set correctly for the response.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <caption>Setting CORS headers for a public request.</caption>
|
|
47
|
+
* <description>Use the `cors` helper to ensure your app can respond to customer account extension requests.</description>
|
|
48
|
+
* ```ts
|
|
49
|
+
* // app/routes/public/my-route.ts
|
|
50
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
51
|
+
* import { authenticate } from "../shopify.server";
|
|
52
|
+
* import { getMyAppData } from "~/db/model.server";
|
|
53
|
+
*
|
|
54
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
55
|
+
* const { sessionToken, cors } = await authenticate.public.customerAccount(
|
|
56
|
+
* request,
|
|
57
|
+
* { corsHeaders: ["X-My-Custom-Header"] }
|
|
58
|
+
* );
|
|
59
|
+
* const data = await getMyAppData({shop: sessionToken.dest});
|
|
60
|
+
* return cors(json(data));
|
|
61
|
+
* };
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
cors: EnsureCORSFunction;
|
|
65
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {BasicParams} from '../../../types';
|
|
2
|
+
import {
|
|
3
|
+
respondToBotRequest,
|
|
4
|
+
respondToOptionsRequest,
|
|
5
|
+
getSessionTokenHeader,
|
|
6
|
+
validateSessionToken,
|
|
7
|
+
ensureCORSHeadersFactory,
|
|
8
|
+
getShopFromRequest,
|
|
9
|
+
} from '../../helpers';
|
|
10
|
+
|
|
11
|
+
import {AuthenticateExtension, ExtensionContext} from './types';
|
|
12
|
+
|
|
13
|
+
export function authenticateExtensionFactory(
|
|
14
|
+
params: BasicParams,
|
|
15
|
+
requestType: string,
|
|
16
|
+
): AuthenticateExtension {
|
|
17
|
+
return async function authenticateExtension(
|
|
18
|
+
request,
|
|
19
|
+
options = {},
|
|
20
|
+
): Promise<ExtensionContext> {
|
|
21
|
+
const {logger} = params;
|
|
22
|
+
|
|
23
|
+
const corsHeaders = options.corsHeaders ?? [];
|
|
24
|
+
|
|
25
|
+
respondToBotRequest(params, request);
|
|
26
|
+
respondToOptionsRequest(params, request, corsHeaders);
|
|
27
|
+
|
|
28
|
+
const sessionTokenHeader = getSessionTokenHeader(request);
|
|
29
|
+
|
|
30
|
+
logger.info(`Authenticating ${requestType} request`, {
|
|
31
|
+
shop: getShopFromRequest(request),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!sessionTokenHeader) {
|
|
35
|
+
logger.debug('Request did not contain a session token', {
|
|
36
|
+
shop: getShopFromRequest(request),
|
|
37
|
+
});
|
|
38
|
+
throw new Response(undefined, {
|
|
39
|
+
status: 401,
|
|
40
|
+
statusText: 'Unauthorized',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
sessionToken: await validateSessionToken(
|
|
46
|
+
params,
|
|
47
|
+
request,
|
|
48
|
+
sessionTokenHeader,
|
|
49
|
+
{checkAudience: false, retryRequest: false},
|
|
50
|
+
),
|
|
51
|
+
cors: ensureCORSHeadersFactory(params, request, corsHeaders),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AuthenticateExtension = (
|
|
2
|
+
request: Request,
|
|
3
|
+
options?: AuthenticateExtensionOptions,
|
|
4
|
+
) => Promise<ExtensionContext>;
|
|
5
|
+
|
|
6
|
+
export interface AuthenticateExtensionOptions {
|
|
7
|
+
corsHeaders?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
11
|
+
export interface ExtensionContext {}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {BasicParams} from '../../types';
|
|
2
|
+
|
|
3
|
+
import {authenticateCheckoutFactory} from './checkout/authenticate';
|
|
4
|
+
import {authenticateAppProxyFactory} from './appProxy/authenticate';
|
|
5
|
+
import {authenticateCustomerAccountFactory} from './customer-account/authenticate';
|
|
6
|
+
import {AuthenticatePublic} from './types';
|
|
7
|
+
import {authenticatePOSFactory} from './pos/authenticate';
|
|
8
|
+
|
|
9
|
+
export function authenticatePublicFactory(params: BasicParams) {
|
|
10
|
+
const authenticateCheckout = authenticateCheckoutFactory(params);
|
|
11
|
+
const authenticateAppProxy = authenticateAppProxyFactory(params);
|
|
12
|
+
const authenticateCustomerAccount =
|
|
13
|
+
authenticateCustomerAccountFactory(params);
|
|
14
|
+
const authenticatePOS = authenticatePOSFactory(params);
|
|
15
|
+
|
|
16
|
+
const context: AuthenticatePublic = {
|
|
17
|
+
checkout: authenticateCheckout,
|
|
18
|
+
appProxy: authenticateAppProxy,
|
|
19
|
+
customerAccount: authenticateCustomerAccount,
|
|
20
|
+
pos: authenticatePOS,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return context;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './factory';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {BasicParams} from '../../../types';
|
|
2
|
+
import {authenticateExtensionFactory} from '../extension/authenticate';
|
|
3
|
+
|
|
4
|
+
import type {AuthenticatePOS} from './types';
|
|
5
|
+
|
|
6
|
+
export function authenticatePOSFactory(params: BasicParams): AuthenticatePOS {
|
|
7
|
+
return authenticateExtensionFactory(params, 'pos') as AuthenticatePOS;
|
|
8
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {JwtPayload} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import {EnsureCORSFunction} from '../../helpers/ensure-cors-headers';
|
|
4
|
+
|
|
5
|
+
export type AuthenticatePOS = (
|
|
6
|
+
request: Request,
|
|
7
|
+
options?: AuthenticatePOSOptions,
|
|
8
|
+
) => Promise<POSContext>;
|
|
9
|
+
|
|
10
|
+
export interface AuthenticatePOSOptions {
|
|
11
|
+
corsHeaders?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Authenticated Context for a POS UI extension request
|
|
16
|
+
*/
|
|
17
|
+
export interface POSContext {
|
|
18
|
+
/**
|
|
19
|
+
* The decoded and validated session token for the request
|
|
20
|
+
*
|
|
21
|
+
* Refer to the OAuth docs for the [session token payload](https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload).
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <caption>Using the decoded session token.</caption>
|
|
25
|
+
* <description>Get store-specific data using the `sessionToken` object.</description>
|
|
26
|
+
* ```ts
|
|
27
|
+
* // app/routes/public/my-route.ts
|
|
28
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
29
|
+
* import { authenticate } from "../shopify.server";
|
|
30
|
+
* import { getMyAppData } from "~/db/model.server";
|
|
31
|
+
*
|
|
32
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
33
|
+
* const { sessionToken } = await authenticate.public.pos(
|
|
34
|
+
* request
|
|
35
|
+
* );
|
|
36
|
+
* return json(await getMyAppData({shop: sessionToken.dest}));
|
|
37
|
+
* };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
sessionToken: JwtPayload;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A function that ensures the CORS headers are set correctly for the response.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <caption>Setting CORS headers for a public request.</caption>
|
|
47
|
+
* <description>Use the `cors` helper to ensure your app can respond to POS UI extension requests.</description>
|
|
48
|
+
* ```ts
|
|
49
|
+
* // app/routes/public/my-route.ts
|
|
50
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
51
|
+
* import { authenticate } from "../shopify.server";
|
|
52
|
+
* import { getMyAppData } from "~/db/model.server";
|
|
53
|
+
*
|
|
54
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
55
|
+
* const { sessionToken, cors } = await authenticate.public.pos(
|
|
56
|
+
* request,
|
|
57
|
+
* { corsHeaders: ["X-My-Custom-Header"] }
|
|
58
|
+
* );
|
|
59
|
+
* const data = await getMyAppData({shop: sessionToken.dest});
|
|
60
|
+
* return cors(json(data));
|
|
61
|
+
* };
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
cors: EnsureCORSFunction;
|
|
65
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {AuthenticateCheckout} from './checkout/types';
|
|
2
|
+
import type {AuthenticateAppProxy} from './appProxy/types';
|
|
3
|
+
import type {AuthenticateCustomerAccount} from './customer-account/types';
|
|
4
|
+
import type {AuthenticatePOS} from './pos/types';
|
|
5
|
+
|
|
6
|
+
// Eventually this will be just the `{}` bit without `AuthenticateCheckout &`
|
|
7
|
+
// We have this is because in v1 public WAS the only public authenticate method
|
|
8
|
+
// But it became tightly coupled to authenticating Checkout requests.
|
|
9
|
+
// In V2 you will have only public.checkout() and public.appProxy(), no public()
|
|
10
|
+
|
|
11
|
+
export interface AuthenticatePublic {
|
|
12
|
+
/**
|
|
13
|
+
* Authenticate a request from a checkout extension
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <caption>Authenticating a checkout extension request</caption>
|
|
17
|
+
* ```ts
|
|
18
|
+
* // /app/routes/public/widgets.ts
|
|
19
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
20
|
+
* import { authenticate } from "../shopify.server";
|
|
21
|
+
*
|
|
22
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
23
|
+
* const { sessionToken, cors } = await authenticate.public.checkout(
|
|
24
|
+
* request,
|
|
25
|
+
* );
|
|
26
|
+
* return cors(json({my: "data", shop: sessionToken.dest}));
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
checkout: AuthenticateCheckout;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authenticate a request from an app proxy
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* <caption>Authenticating an app proxy request</caption>
|
|
37
|
+
* ```ts
|
|
38
|
+
* // /app/routes/public/widgets.ts
|
|
39
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
40
|
+
* import { authenticate } from "../shopify.server";
|
|
41
|
+
*
|
|
42
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
43
|
+
* await authenticate.public.appProxy(
|
|
44
|
+
* request,
|
|
45
|
+
* );
|
|
46
|
+
*
|
|
47
|
+
* const {searchParams} = new URL(request.url);
|
|
48
|
+
* const shop = searchParams.get("shop");
|
|
49
|
+
* const customerId = searchParams.get("logged_in_customer_id")
|
|
50
|
+
*
|
|
51
|
+
* return json({my: "data", shop, customerId});
|
|
52
|
+
* };
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
appProxy: AuthenticateAppProxy;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Authenticate a request from a customer account extension
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* <caption>Authenticating a customer account extension request</caption>
|
|
62
|
+
* ```ts
|
|
63
|
+
* // /app/routes/public/widgets.ts
|
|
64
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
65
|
+
* import { authenticate } from "../shopify.server";
|
|
66
|
+
*
|
|
67
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
68
|
+
* const { sessionToken, cors } = await authenticate.public.customerAccount(
|
|
69
|
+
* request,
|
|
70
|
+
* );
|
|
71
|
+
* return cors(json({my: "data", shop: sessionToken.dest}));
|
|
72
|
+
* };
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
customerAccount: AuthenticateCustomerAccount;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Authenticate a request from a POS UI extension
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* <caption>Authenticating a POS UI extension request</caption>
|
|
82
|
+
* ```ts
|
|
83
|
+
* // /app/routes/public/widgets.ts
|
|
84
|
+
* import { LoaderFunctionArgs, json } from "@remix-run/node";
|
|
85
|
+
* import { authenticate } from "../shopify.server";
|
|
86
|
+
*
|
|
87
|
+
* export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
88
|
+
* const { sessionToken, cors } = await authenticate.public.pos(
|
|
89
|
+
* request,
|
|
90
|
+
* );
|
|
91
|
+
* return cors(json({my: "data", shop: sessionToken.dest}));
|
|
92
|
+
* };
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
pos: AuthenticatePOS;
|
|
96
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {WebhookValidationErrorReason, WebhookType} from '@shopify/shopify-api';
|
|
2
|
+
|
|
3
|
+
import type {BasicParams} from '../../types';
|
|
4
|
+
import {adminClientFactory} from '../../clients';
|
|
5
|
+
import {handleClientErrorFactory} from '../admin/helpers';
|
|
6
|
+
import {ensureValidOfflineSession} from '../../helpers';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
AuthenticateWebhook,
|
|
10
|
+
WebhookContext,
|
|
11
|
+
WebhookContextWithoutSession,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
export function authenticateWebhookFactory<Topics extends string>(
|
|
15
|
+
params: BasicParams,
|
|
16
|
+
): AuthenticateWebhook<Topics> {
|
|
17
|
+
const {api, logger} = params;
|
|
18
|
+
|
|
19
|
+
return async function authenticate(
|
|
20
|
+
request: Request,
|
|
21
|
+
): Promise<WebhookContext<Topics>> {
|
|
22
|
+
if (request.method !== 'POST') {
|
|
23
|
+
logger.debug(
|
|
24
|
+
'Received a non-POST request for a webhook. Only POST requests are allowed.',
|
|
25
|
+
{url: request.url, method: request.method},
|
|
26
|
+
);
|
|
27
|
+
throw new Response(undefined, {
|
|
28
|
+
status: 405,
|
|
29
|
+
statusText: 'Method not allowed',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rawBody = await request.text();
|
|
34
|
+
|
|
35
|
+
const check = await api.webhooks.validate({
|
|
36
|
+
rawBody,
|
|
37
|
+
rawRequest: request,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!check.valid) {
|
|
41
|
+
if (check.reason === WebhookValidationErrorReason.InvalidHmac) {
|
|
42
|
+
logger.debug('Webhook HMAC validation failed', check);
|
|
43
|
+
throw new Response(undefined, {
|
|
44
|
+
status: 401,
|
|
45
|
+
statusText: 'Unauthorized',
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
logger.debug('Webhook validation failed', check);
|
|
49
|
+
throw new Response(undefined, {status: 400, statusText: 'Bad Request'});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const session = await ensureValidOfflineSession(params, check.domain);
|
|
53
|
+
|
|
54
|
+
let webhookContext: WebhookContextWithoutSession<Topics>;
|
|
55
|
+
|
|
56
|
+
if (check.webhookType === WebhookType.Webhooks) {
|
|
57
|
+
webhookContext = {
|
|
58
|
+
apiVersion: check.apiVersion,
|
|
59
|
+
shop: check.domain,
|
|
60
|
+
topic: check.topic as Topics,
|
|
61
|
+
webhookId: check.webhookId,
|
|
62
|
+
payload: JSON.parse(rawBody),
|
|
63
|
+
subTopic: check.subTopic || undefined,
|
|
64
|
+
session: undefined,
|
|
65
|
+
admin: undefined,
|
|
66
|
+
webhookType: check.webhookType,
|
|
67
|
+
name: check.name,
|
|
68
|
+
triggeredAt: check.triggeredAt,
|
|
69
|
+
eventId: check.eventId,
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
webhookContext = {
|
|
73
|
+
apiVersion: check.apiVersion,
|
|
74
|
+
shop: check.domain,
|
|
75
|
+
topic: check.topic as Topics,
|
|
76
|
+
webhookId: check.eventId,
|
|
77
|
+
payload: JSON.parse(rawBody),
|
|
78
|
+
session: undefined,
|
|
79
|
+
admin: undefined,
|
|
80
|
+
webhookType: check.webhookType,
|
|
81
|
+
handle: check.handle,
|
|
82
|
+
action: check.action,
|
|
83
|
+
resourceId: check.resourceId,
|
|
84
|
+
triggeredAt: check.triggeredAt,
|
|
85
|
+
eventId: check.eventId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!session) {
|
|
90
|
+
return webhookContext;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const admin = adminClientFactory({
|
|
94
|
+
params,
|
|
95
|
+
session,
|
|
96
|
+
handleClientError: handleClientErrorFactory({request}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...webhookContext,
|
|
101
|
+
session,
|
|
102
|
+
admin,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {type ActionFunctionArgs} from '@remix-run/node';
|
|
2
|
+
|
|
3
|
+
import {authenticate} from '../shopify.server';
|
|
4
|
+
|
|
5
|
+
export const action = async ({request}: ActionFunctionArgs) => {
|
|
6
|
+
const {topic, admin, payload, session} = await authenticate.webhook(request);
|
|
7
|
+
|
|
8
|
+
// Webhook requests can trigger after an app is uninstalled
|
|
9
|
+
// If the app is already uninstalled, the session may be undefined.
|
|
10
|
+
if (!session) {
|
|
11
|
+
throw new Response();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
switch (topic) {
|
|
15
|
+
case 'PRODUCTS_UPDATE':
|
|
16
|
+
await admin.graphql(
|
|
17
|
+
`#graphql
|
|
18
|
+
mutation setMetafield($productId: ID!, $time: String!) {
|
|
19
|
+
metafieldsSet(metafields: {
|
|
20
|
+
ownerId: $productId
|
|
21
|
+
namespace: "my-app",
|
|
22
|
+
key: "webhook_received_at",
|
|
23
|
+
value: $time,
|
|
24
|
+
type: "string",
|
|
25
|
+
}) {
|
|
26
|
+
metafields {
|
|
27
|
+
key
|
|
28
|
+
value
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`,
|
|
33
|
+
{
|
|
34
|
+
variables: {
|
|
35
|
+
productId: payload.admin_graphql_api_id,
|
|
36
|
+
time: new Date().toISOString(),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Response();
|
|
43
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';
|
|
2
|
+
|
|
3
|
+
const data: ReferenceEntityTemplateSchema = {
|
|
4
|
+
name: 'Webhook',
|
|
5
|
+
description: 'Contains functions for verifying Shopify webhooks.',
|
|
6
|
+
category: 'Authenticate',
|
|
7
|
+
type: 'object',
|
|
8
|
+
isVisualComponent: false,
|
|
9
|
+
definitions: [
|
|
10
|
+
{
|
|
11
|
+
title: 'authenticate.webhook',
|
|
12
|
+
description: 'Verifies requests coming from Shopify webhooks.',
|
|
13
|
+
type: 'AuthenticateWebhook',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
defaultExample: {
|
|
17
|
+
description: 'Update a metafield when a product is updated',
|
|
18
|
+
codeblock: {
|
|
19
|
+
title: 'Update a metafield when a product is updated',
|
|
20
|
+
tabs: [
|
|
21
|
+
{
|
|
22
|
+
title: '/app/routes/**.ts',
|
|
23
|
+
language: 'typescript',
|
|
24
|
+
code: './authenticate.webhooks.doc.example.ts',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
jsDocTypeExamples: ['WebhookContextWithSession'],
|
|
30
|
+
related: [
|
|
31
|
+
{
|
|
32
|
+
name: 'Admin API context',
|
|
33
|
+
subtitle: 'Interact with the Admin API.',
|
|
34
|
+
url: '/docs/api/shopify-app-remix/apis/admin-api',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default data;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {registerWebhooksFactory} from './register';
|