From c9b8c725d74388c16c11dba585f182c1ff200a29 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 8 Sep 2023 10:11:48 -0400 Subject: [PATCH 01/14] Add an unauthenticated Storefront client --- .../src/server/clients/admin/factory.ts | 1 + .../src/server/clients/admin/graphql.ts | 20 ++++--------- .../src/server/clients/storefront/factory.ts | 29 +++++++++++++++++++ .../src/server/clients/storefront/index.ts | 1 + .../src/server/clients/types.ts | 17 +++++++++++ .../src/server/config-types.ts | 10 +++++-- .../src/server/shopify-app.ts | 16 ++++++++++ .../shopify-app-remix/src/server/types.ts | 11 ++++--- .../server/unauthenticated/admin/factory.ts | 6 ++-- .../helpers/get-offline-session.ts | 12 ++++++++ .../server/unauthenticated/helpers/index.ts | 1 + .../unauthenticated/storefront/factory.ts | 25 ++++++++++++++++ .../unauthenticated/storefront/index.ts | 1 + .../unauthenticated/storefront/types.ts | 10 +++++++ 14 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 packages/shopify-app-remix/src/server/clients/storefront/factory.ts create mode 100644 packages/shopify-app-remix/src/server/clients/storefront/index.ts create mode 100644 packages/shopify-app-remix/src/server/clients/types.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/helpers/get-offline-session.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/helpers/index.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/storefront/index.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts diff --git a/packages/shopify-app-remix/src/server/clients/admin/factory.ts b/packages/shopify-app-remix/src/server/clients/admin/factory.ts index 967b5212a2..bdb4311361 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/factory.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/factory.ts @@ -11,6 +11,7 @@ interface RestClientOptions { session: Session; handleClientError?: (error: any) => Promise; } + export function adminClientFactory< Resources extends ShopifyRestResources = ShopifyRestResources, >({ diff --git a/packages/shopify-app-remix/src/server/clients/admin/graphql.ts b/packages/shopify-app-remix/src/server/clients/admin/graphql.ts index e6ea01b050..aa0c16df6a 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/graphql.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/graphql.ts @@ -1,22 +1,12 @@ -import {ApiVersion} from '@shopify/shopify-api'; import {flatHeaders} from '@shopify/shopify-api/runtime'; -import {AdminClientOptions} from './types'; - -interface QueryVariables { - [key: string]: any; -} +import {GraphQLClient, GraphQLQueryOptions} from '../types'; -interface QueryOptions { - variables?: QueryVariables; - apiVersion?: ApiVersion; - headers?: {[key: string]: any}; - tries?: number; -} +import {AdminClientOptions} from './types'; export type GraphqlQueryFunction = ( query: string, - options?: QueryOptions, + options?: GraphQLQueryOptions, ) => Promise; // eslint-disable-next-line no-warning-comments @@ -27,8 +17,8 @@ export function graphqlClientFactory({ params, handleClientError, session, -}: AdminClientOptions) { - return async function query(query: string, options?: QueryOptions) { +}: AdminClientOptions): GraphQLClient { + return async function query(query, options) { const client = new params.api.clients.Graphql({ session, apiVersion: options?.apiVersion, diff --git a/packages/shopify-app-remix/src/server/clients/storefront/factory.ts b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts new file mode 100644 index 0000000000..c99004f232 --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts @@ -0,0 +1,29 @@ +import {flatHeaders} from '@shopify/shopify-api/runtime'; +import {Session} from '@shopify/shopify-api'; +import {BasicParams} from 'src/server/types'; + +import {GraphQLClient} from '../types'; + +export function storefrontGraphQLClientFactory( + params: BasicParams, + session: Session, +): GraphQLClient { + const {api} = params; + + return async (query, options = {}) => { + const client = new api.clients.Storefront({ + session, + apiVersion: options.apiVersion, + }); + + const apiResponse = await client.query({ + data: {query, variables: options?.variables}, + tries: options.tries, + extraHeaders: options.headers, + }); + + return new Response(JSON.stringify(apiResponse.body), { + headers: flatHeaders(apiResponse.headers), + }); + }; +} diff --git a/packages/shopify-app-remix/src/server/clients/storefront/index.ts b/packages/shopify-app-remix/src/server/clients/storefront/index.ts new file mode 100644 index 0000000000..d847d7abce --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/storefront/index.ts @@ -0,0 +1 @@ +export * from './factory'; diff --git a/packages/shopify-app-remix/src/server/clients/types.ts b/packages/shopify-app-remix/src/server/clients/types.ts new file mode 100644 index 0000000000..1bbc348c6d --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/types.ts @@ -0,0 +1,17 @@ +import {ApiVersion} from '@shopify/shopify-api'; + +interface QueryVariables { + [key: string]: any; +} + +export interface GraphQLQueryOptions { + variables?: QueryVariables; + apiVersion?: ApiVersion; + headers?: {[key: string]: any}; + tries?: number; +} + +export type GraphQLClient = ( + query: string, + options?: GraphQLQueryOptions, +) => Promise; diff --git a/packages/shopify-app-remix/src/server/config-types.ts b/packages/shopify-app-remix/src/server/config-types.ts index f668d78b35..97825b994f 100644 --- a/packages/shopify-app-remix/src/server/config-types.ts +++ b/packages/shopify-app-remix/src/server/config-types.ts @@ -10,7 +10,7 @@ import {SessionStorage} from '@shopify/shopify-app-session-storage'; import {AppDistribution} from './types'; import {RestClientWithResources} from './clients/admin/rest'; -import {GraphqlQueryFunction} from './clients/admin/graphql'; +import {GraphQLClient} from './clients/types'; export interface AppConfigArg< Resources extends ShopifyRestResources = ShopifyRestResources, @@ -212,6 +212,12 @@ export interface AppConfigArg< * ``` */ authPathPrefix?: string; + + /** + * TODO: Document + * + */ + privateStorefrontAccessToken?: string; } export interface AppConfig @@ -367,5 +373,5 @@ export interface AdminApiContext< * } * ``` */ - graphql: GraphqlQueryFunction; + graphql: GraphQLClient; } diff --git a/packages/shopify-app-remix/src/server/shopify-app.ts b/packages/shopify-app-remix/src/server/shopify-app.ts index e35f5d0da3..0595c721d0 100644 --- a/packages/shopify-app-remix/src/server/shopify-app.ts +++ b/packages/shopify-app-remix/src/server/shopify-app.ts @@ -30,6 +30,7 @@ import {addDocumentResponseHeadersFactory} from './authenticate/helpers'; import {loginFactory} from './authenticate/login/login'; import {unauthenticatedAdminContextFactory} from './unauthenticated/admin'; import {authenticatePublicFactory} from './authenticate/public'; +import {unauthenticatedStorefrontContextFactory} from './unauthenticated/storefront'; /** * Creates an object your app will use to interact with Shopify. @@ -67,6 +68,13 @@ export function shopifyApp< const params: BasicParams = {api, config, logger}; const oauth = new AuthStrategy(params); + if (appConfig.privateAppStorefrontAccessToken) { + logger.deprecated( + '2.0.0', + 'privateAppStorefrontAccessToken is deprecated. Use privateStorefrontAccessToken instead.', + ); + } + const shopify: | AdminApp | AppStoreApp @@ -84,6 +92,7 @@ export function shopifyApp< }, unauthenticated: { admin: unauthenticatedAdminContextFactory(params), + storefront: unauthenticatedStorefrontContextFactory(params), }, }; @@ -161,9 +170,16 @@ function deriveConfig( const authPathPrefix = appConfig.authPathPrefix || '/auth'; appConfig.distribution = appConfig.distribution ?? AppDistribution.AppStore; + const privateAppStorefrontAccessToken = + appConfig.privateStorefrontAccessToken || + appConfig.privateAppStorefrontAccessToken; + + delete appConfig.privateStorefrontAccessToken; + return { ...appConfig, ...apiConfig, + privateAppStorefrontAccessToken, canUseLoginForm: appConfig.distribution !== AppDistribution.ShopifyAdmin, useOnlineTokens: appConfig.useOnlineTokens ?? false, hooks: appConfig.hooks ?? {}, diff --git a/packages/shopify-app-remix/src/server/types.ts b/packages/shopify-app-remix/src/server/types.ts index 9a3679a4f8..25d7a8e741 100644 --- a/packages/shopify-app-remix/src/server/types.ts +++ b/packages/shopify-app-remix/src/server/types.ts @@ -218,10 +218,10 @@ interface Unauthenticated { * This method throws an error if there is no session for the shop. * * @example - * Responding to a request from an external service not controlled by Shopify. + * Responding to a request from an external service not controlled by Shopify. * ```ts - * // /app/shopify.server.ts - * import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix/server"; + * // app/shopify.server.ts + * import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ @@ -229,9 +229,8 @@ interface Unauthenticated { * // ...etc * }); * export default shopify; - * ``` - * ```ts - * // /app/routes/**\/*.jsx + * + * // app/routes/**\/*.jsx * import { LoaderArgs, json } from "@remix-run/node"; * import { authenticateExternal } from "~/helpers/authenticate" * import shopify from "../../shopify.server"; diff --git a/packages/shopify-app-remix/src/server/unauthenticated/admin/factory.ts b/packages/shopify-app-remix/src/server/unauthenticated/admin/factory.ts index 7b46a85cbf..06bd9b2c9c 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/admin/factory.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/admin/factory.ts @@ -2,6 +2,7 @@ import {ShopifyError, ShopifyRestResources} from '@shopify/shopify-api'; import {BasicParams} from '../../types'; import {adminClientFactory} from '../../clients/admin'; +import {getOfflineSession} from '../helpers'; import {UnauthenticatedAdminContext} from './types'; @@ -11,10 +12,7 @@ export function unauthenticatedAdminContextFactory< return async ( shop: string, ): Promise> => { - const offlineSessionId = params.api.session.getOfflineId(shop); - const session = await params.config.sessionStorage.loadSession( - offlineSessionId, - ); + const session = await getOfflineSession(shop, params); if (!session) { throw new ShopifyError( diff --git a/packages/shopify-app-remix/src/server/unauthenticated/helpers/get-offline-session.ts b/packages/shopify-app-remix/src/server/unauthenticated/helpers/get-offline-session.ts new file mode 100644 index 0000000000..c367c11f74 --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/helpers/get-offline-session.ts @@ -0,0 +1,12 @@ +import {Session} from '@shopify/shopify-api'; +import {BasicParams} from 'src/server/types'; + +export async function getOfflineSession( + shop: string, + {api, config}: BasicParams, +): Promise { + const offlineSessionId = api.session.getOfflineId(shop); + const session = await config.sessionStorage.loadSession(offlineSessionId); + + return session; +} diff --git a/packages/shopify-app-remix/src/server/unauthenticated/helpers/index.ts b/packages/shopify-app-remix/src/server/unauthenticated/helpers/index.ts new file mode 100644 index 0000000000..2a900cc08f --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/helpers/index.ts @@ -0,0 +1 @@ +export * from './get-offline-session'; diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts new file mode 100644 index 0000000000..2578154138 --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts @@ -0,0 +1,25 @@ +import {BasicParams} from 'src/server/types'; +import {ShopifyError} from '@shopify/shopify-api'; + +import {storefrontGraphQLClientFactory} from '../../clients/storefront'; +import {getOfflineSession} from '../helpers'; + +import {StorefrontContext, GetStorefrontContext} from './types'; + +export function unauthenticatedStorefrontContextFactory( + params: BasicParams, +): GetStorefrontContext { + return async (shop: string): Promise => { + const session = await getOfflineSession(shop, params); + + if (!session) { + throw new ShopifyError( + `Could not find a session for shop ${shop} when creating unauthenticated admin context`, + ); + } + + return { + graphql: storefrontGraphQLClientFactory(params, session), + }; + }; +} diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/index.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/index.ts new file mode 100644 index 0000000000..d847d7abce --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/index.ts @@ -0,0 +1 @@ +export * from './factory'; diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts new file mode 100644 index 0000000000..404c9ad491 --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts @@ -0,0 +1,10 @@ +import {GraphQLClient} from '../../clients/types'; + +export interface StorefrontContext { + /** + * TODO: Add TSDoc + */ + graphql: GraphQLClient; +} + +export type GetStorefrontContext = (shop: string) => Promise; From ec14959c85e7cc04418df39e25bb6ef0377398bb Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 10:05:34 -0400 Subject: [PATCH 02/14] Authenticate.public.appProxy now returns a storefront API client --- .../authenticate/public/appProxy/authenticate.ts | 4 +++- .../server/authenticate/public/appProxy/types.ts | 13 +++++++++++++ .../shopify-app-remix/src/server/clients/index.ts | 2 ++ .../src/server/clients/storefront/factory.ts | 13 ++++++++----- .../server/unauthenticated/storefront/factory.ts | 4 ++-- 5 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 packages/shopify-app-remix/src/server/clients/index.ts diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.ts index 62386fba05..014ffe650b 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.ts @@ -1,6 +1,6 @@ import {ShopifyRestResources} from '@shopify/shopify-api'; -import {adminClientFactory} from '../../../clients/admin'; +import {adminClientFactory, storefrontClientFactory} from '../../../clients'; import {BasicParams} from '../../../types'; import { @@ -50,6 +50,7 @@ export function authenticateAppProxyFactory< liquid, session: undefined, admin: undefined, + storefront: undefined, }; return context; @@ -59,6 +60,7 @@ export function authenticateAppProxyFactory< liquid, session, admin: adminClientFactory({params, session}), + storefront: storefrontClientFactory({params, session}), }; return context; diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts index bb5e94fea5..6bd888b0e6 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts @@ -1,6 +1,7 @@ import {Session, ShopifyRestResources} from '@shopify/shopify-api'; import {AdminApiContext} from '../../../config-types'; +import {GraphQLClient} from '../../../clients/types'; export type AuthenticateAppProxy = ( request: Request, @@ -50,6 +51,12 @@ export interface AppProxyContext extends Context { * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available. */ admin: undefined; + + /** + * No session is available for the shop that made this request. + * Therefore no method for interacting with the Storefront API is available. + */ + storefront: undefined; } export interface AppProxyContextWithSession< @@ -111,4 +118,10 @@ export interface AppProxyContextWithSession< * ``` */ admin: AdminApiContext; + + /** + * TODO + * + */ + storefront: GraphQLClient; } diff --git a/packages/shopify-app-remix/src/server/clients/index.ts b/packages/shopify-app-remix/src/server/clients/index.ts new file mode 100644 index 0000000000..be1decf9df --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/index.ts @@ -0,0 +1,2 @@ +export * from './admin'; +export * from './storefront'; diff --git a/packages/shopify-app-remix/src/server/clients/storefront/factory.ts b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts index c99004f232..e3e1f2ae98 100644 --- a/packages/shopify-app-remix/src/server/clients/storefront/factory.ts +++ b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts @@ -1,13 +1,16 @@ import {flatHeaders} from '@shopify/shopify-api/runtime'; import {Session} from '@shopify/shopify-api'; -import {BasicParams} from 'src/server/types'; +import {BasicParams} from '../../types'; import {GraphQLClient} from '../types'; -export function storefrontGraphQLClientFactory( - params: BasicParams, - session: Session, -): GraphQLClient { +export function storefrontClientFactory({ + params, + session, +}: { + params: BasicParams; + session: Session; +}): GraphQLClient { const {api} = params; return async (query, options = {}) => { diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts index 2578154138..907fe133a5 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts @@ -1,7 +1,7 @@ import {BasicParams} from 'src/server/types'; import {ShopifyError} from '@shopify/shopify-api'; -import {storefrontGraphQLClientFactory} from '../../clients/storefront'; +import {storefrontClientFactory} from '../../clients/storefront'; import {getOfflineSession} from '../helpers'; import {StorefrontContext, GetStorefrontContext} from './types'; @@ -19,7 +19,7 @@ export function unauthenticatedStorefrontContextFactory( } return { - graphql: storefrontGraphQLClientFactory(params, session), + graphql: storefrontClientFactory({params, session}), }; }; } From 748858c300033be6dd3ef03d08bbff8361076f36 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:50:53 -0400 Subject: [PATCH 03/14] Standardize storefront context object --- .../src/server/authenticate/admin/types.ts | 8 +- .../authenticate/public/appProxy/types.ts | 5 +- .../src/server/authenticate/webhooks/types.ts | 9 ++ .../src/server/clients/admin/index.ts | 1 + .../src/server/clients/admin/rest.ts | 2 +- .../src/server/clients/admin/types.ts | 102 +++++++++++++++++- .../src/server/clients/storefront/factory.ts | 33 +++--- .../src/server/clients/storefront/index.ts | 1 + .../src/server/clients/storefront/types.ts | 5 + .../src/server/config-types.ts | 3 +- .../shopify-app-remix/src/server/types.ts | 5 +- .../src/server/unauthenticated/admin/types.ts | 6 +- .../unauthenticated/storefront/factory.ts | 12 ++- .../unauthenticated/storefront/types.ts | 17 ++- 14 files changed, 173 insertions(+), 36 deletions(-) create mode 100644 packages/shopify-app-remix/src/server/clients/storefront/types.ts diff --git a/packages/shopify-app-remix/src/server/authenticate/admin/types.ts b/packages/shopify-app-remix/src/server/authenticate/admin/types.ts index b6579fd9f1..e7a263367f 100644 --- a/packages/shopify-app-remix/src/server/authenticate/admin/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/admin/types.ts @@ -1,7 +1,8 @@ import {JwtPayload, Session, ShopifyRestResources} from '@shopify/shopify-api'; import {EnsureCORSFunction} from '../helpers/ensure-cors-headers'; -import type {AdminApiContext, AppConfigArg} from '../../config-types'; +import type {AppConfigArg} from '../../config-types'; +import type {AdminApiContext} from '../../clients'; import type {BillingContext} from './billing/types'; import {RedirectFunction} from './helpers/redirect'; @@ -191,3 +192,8 @@ export type AdminContext< > = Config['isEmbeddedApp'] extends false ? NonEmbeddedAdminContext : EmbeddedAdminContext; + +export type AuthenticateAdmin< + Config extends AppConfigArg, + Resources extends ShopifyRestResources = ShopifyRestResources, +> = (request: Request) => Promise>; diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts index 6bd888b0e6..e55e62e37f 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts @@ -1,7 +1,6 @@ import {Session, ShopifyRestResources} from '@shopify/shopify-api'; -import {AdminApiContext} from '../../../config-types'; -import {GraphQLClient} from '../../../clients/types'; +import {AdminApiContext, StorefrontContext} from '../../../clients'; export type AuthenticateAppProxy = ( request: Request, @@ -123,5 +122,5 @@ export interface AppProxyContextWithSession< * TODO * */ - storefront: GraphQLClient; + storefront: StorefrontContext; } diff --git a/packages/shopify-app-remix/src/server/authenticate/webhooks/types.ts b/packages/shopify-app-remix/src/server/authenticate/webhooks/types.ts index acdcee3e83..93b19e7e6f 100644 --- a/packages/shopify-app-remix/src/server/authenticate/webhooks/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/webhooks/types.ts @@ -132,3 +132,12 @@ export interface WebhookContextWithSession< graphql: InstanceType; }; } + +export type AuthenticateWebhook< + Resources extends ShopifyRestResources = ShopifyRestResources, + Topics = string | number | symbol, +> = ( + request: Request, +) => Promise< + WebhookContext | WebhookContextWithSession +>; diff --git a/packages/shopify-app-remix/src/server/clients/admin/index.ts b/packages/shopify-app-remix/src/server/clients/admin/index.ts index d847d7abce..93f2263b54 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/index.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/index.ts @@ -1 +1,2 @@ export * from './factory'; +export * from './types'; diff --git a/packages/shopify-app-remix/src/server/clients/admin/rest.ts b/packages/shopify-app-remix/src/server/clients/admin/rest.ts index d087d038a2..d9cc75bb8f 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/rest.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/rest.ts @@ -9,7 +9,7 @@ import { ShopifyRestResources, } from '@shopify/shopify-api'; -import {AdminClientOptions} from './types'; +import type {AdminClientOptions} from './types'; export type RestClientWithResources = RemixRestClient & {resources: Resources}; diff --git a/packages/shopify-app-remix/src/server/clients/admin/types.ts b/packages/shopify-app-remix/src/server/clients/admin/types.ts index 80f9e699c1..c442a67018 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/types.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/types.ts @@ -1,6 +1,9 @@ -import {Session} from '@shopify/shopify-api'; +import {Session, ShopifyRestResources} from '@shopify/shopify-api'; import {BasicParams} from '../../types'; +import {GraphQLClient} from '../types'; + +import type {RestClientWithResources} from './rest'; export interface AdminClientOptions { params: BasicParams; @@ -13,3 +16,100 @@ export type HandleAdminClientError = (arg: { params: BasicParams; session: Session; }) => Promise; + +export interface AdminApiContext< + Resources extends ShopifyRestResources = ShopifyRestResources, +> { + /** + * Methods for interacting with the Shopify Admin REST API + * + * There are methods for interacting with individual REST resources. You can also make plain `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. + * + * {@link https://shopify.dev/docs/api/admin-rest} + * + * @example + * Getting the number of orders in a store using rest resources + * ```ts + * // app/shopify.server.ts + * import { shopifyApp } from "@shopify/shopify-app-remix"; + * import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; + * + * const shopify = shopifyApp({ + * restResources, + * // ...etc + * }); + * export default shopify; + * export const authenticate = shopify.authenticate; + * + * // app/routes/**\/.ts + * import { LoaderArgs, json } from "@remix-run/node"; + * import { authenticate } from "../shopify.server"; + * + * export const loader = async ({ request }: LoaderArgs) => { + * const { admin, session } = await authenticate.admin(request); + * return json(admin.rest.resources.Order.count({ session })); + * }; + * ``` + * + * @example + * Making a GET request to the REST API + * ```ts + * // app/shopify.server.ts + * import { shopifyApp } from "@shopify/shopify-app-remix"; + * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; + * + * const shopify = shopifyApp({ + * restResources, + * // ...etc + * }); + * export default shopify; + * export const authenticate = shopify.authenticate; + * + * // app/routes/**\/.ts + * import { LoaderArgs, json } from "@remix-run/node"; + * import { authenticate } from "../shopify.server"; + * + * export const loader = async ({ request }: LoaderArgs) => { + * const { admin, session } = await authenticate.admin(request); + * const response = await admin.rest.get({ path: "/customers/count.json" }); + * const customers = await response.json(); + * return json({ customers }); + * }; + * ``` + */ + rest: RestClientWithResources; + + /** + * Methods for interacting with the Shopify Admin GraphQL API + * + * {@link https://shopify.dev/docs/api/admin-graphql} + * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md} + * + * @example + * Creating a new product + * ```ts + * import { ActionArgs } from "@remix-run/node"; + * import { authenticate } from "../shopify.server"; + * + * export async function action({ request }: ActionArgs) { + * const { admin } = await authenticate.admin(request); + * + * const response = await admin.graphql( + * `#graphql + * mutation populateProduct($input: ProductInput!) { + * productCreate(input: $input) { + * product { + * id + * } + * } + * }`, + * { variables: { input: { title: "Product Name" } } } + * ); + * + * const productData = await response.json(); + * return json({ data: productData.data }); + * } + * ``` + */ + graphql: GraphQLClient; +} diff --git a/packages/shopify-app-remix/src/server/clients/storefront/factory.ts b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts index e3e1f2ae98..b19f1782e0 100644 --- a/packages/shopify-app-remix/src/server/clients/storefront/factory.ts +++ b/packages/shopify-app-remix/src/server/clients/storefront/factory.ts @@ -2,7 +2,8 @@ import {flatHeaders} from '@shopify/shopify-api/runtime'; import {Session} from '@shopify/shopify-api'; import {BasicParams} from '../../types'; -import {GraphQLClient} from '../types'; + +import type {StorefrontContext} from '.'; export function storefrontClientFactory({ params, @@ -10,23 +11,25 @@ export function storefrontClientFactory({ }: { params: BasicParams; session: Session; -}): GraphQLClient { +}): StorefrontContext { const {api} = params; - return async (query, options = {}) => { - const client = new api.clients.Storefront({ - session, - apiVersion: options.apiVersion, - }); + return { + graphql: async (query, options = {}) => { + const client = new api.clients.Storefront({ + session, + apiVersion: options.apiVersion, + }); - const apiResponse = await client.query({ - data: {query, variables: options?.variables}, - tries: options.tries, - extraHeaders: options.headers, - }); + const apiResponse = await client.query({ + data: {query, variables: options?.variables}, + tries: options.tries, + extraHeaders: options.headers, + }); - return new Response(JSON.stringify(apiResponse.body), { - headers: flatHeaders(apiResponse.headers), - }); + return new Response(JSON.stringify(apiResponse.body), { + headers: flatHeaders(apiResponse.headers), + }); + }, }; } diff --git a/packages/shopify-app-remix/src/server/clients/storefront/index.ts b/packages/shopify-app-remix/src/server/clients/storefront/index.ts index d847d7abce..93f2263b54 100644 --- a/packages/shopify-app-remix/src/server/clients/storefront/index.ts +++ b/packages/shopify-app-remix/src/server/clients/storefront/index.ts @@ -1 +1,2 @@ export * from './factory'; +export * from './types'; diff --git a/packages/shopify-app-remix/src/server/clients/storefront/types.ts b/packages/shopify-app-remix/src/server/clients/storefront/types.ts new file mode 100644 index 0000000000..00d70f630d --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/storefront/types.ts @@ -0,0 +1,5 @@ +import {GraphQLClient} from '../types'; + +export interface StorefrontContext { + graphql: GraphQLClient; +} diff --git a/packages/shopify-app-remix/src/server/config-types.ts b/packages/shopify-app-remix/src/server/config-types.ts index 97825b994f..5e076211ce 100644 --- a/packages/shopify-app-remix/src/server/config-types.ts +++ b/packages/shopify-app-remix/src/server/config-types.ts @@ -9,8 +9,7 @@ import { import {SessionStorage} from '@shopify/shopify-app-session-storage'; import {AppDistribution} from './types'; -import {RestClientWithResources} from './clients/admin/rest'; -import {GraphQLClient} from './clients/types'; +import type {AdminApiContext} from './clients'; export interface AppConfigArg< Resources extends ShopifyRestResources = ShopifyRestResources, diff --git a/packages/shopify-app-remix/src/server/types.ts b/packages/shopify-app-remix/src/server/types.ts index 25d7a8e741..3f0d745a04 100644 --- a/packages/shopify-app-remix/src/server/types.ts +++ b/packages/shopify-app-remix/src/server/types.ts @@ -6,13 +6,10 @@ import { import {SessionStorage} from '@shopify/shopify-app-session-storage'; import type {AppConfig, AppConfigArg} from './config-types'; -import type {AdminContext} from './authenticate/admin/types'; import type { + AuthenticateWebhook, RegisterWebhooksOptions, - WebhookContext, - WebhookContextWithSession, } from './authenticate/webhooks/types'; -import type {UnauthenticatedAdminContext} from './unauthenticated/admin/types'; import type {AuthenticatePublic} from './authenticate/public/types'; export interface BasicParams { diff --git a/packages/shopify-app-remix/src/server/unauthenticated/admin/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/admin/types.ts index f0b27fea07..370f1a9b93 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/admin/types.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/admin/types.ts @@ -1,6 +1,6 @@ import {Session, ShopifyRestResources} from '@shopify/shopify-api'; -import type {AdminApiContext} from '../../config-types'; +import {AdminApiContext} from '../../clients'; export interface UnauthenticatedAdminContext< Resources extends ShopifyRestResources, @@ -35,3 +35,7 @@ export interface UnauthenticatedAdminContext< */ admin: AdminApiContext; } + +export type GetUnauthenticatedAdminContext< + Resources extends ShopifyRestResources, +> = (shop: string) => Promise>; diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts index 907fe133a5..fda1c3a9cf 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts @@ -4,12 +4,15 @@ import {ShopifyError} from '@shopify/shopify-api'; import {storefrontClientFactory} from '../../clients/storefront'; import {getOfflineSession} from '../helpers'; -import {StorefrontContext, GetStorefrontContext} from './types'; +import { + UnauthenticatedStorefrontContext, + GetUnauthenticatedStorefrontContext, +} from './types'; export function unauthenticatedStorefrontContextFactory( params: BasicParams, -): GetStorefrontContext { - return async (shop: string): Promise => { +): GetUnauthenticatedStorefrontContext { + return async (shop: string): Promise => { const session = await getOfflineSession(shop, params); if (!session) { @@ -19,7 +22,8 @@ export function unauthenticatedStorefrontContextFactory( } return { - graphql: storefrontClientFactory({params, session}), + session, + storefront: storefrontClientFactory({params, session}), }; }; } diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts index 404c9ad491..ee5afe219c 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts @@ -1,10 +1,19 @@ -import {GraphQLClient} from '../../clients/types'; +import {Session} from '@shopify/shopify-api'; + +import type {StorefrontContext} from '../../clients'; + +export interface UnauthenticatedStorefrontContext { + /** + * TODO: Add TSDoc + */ + session: Session; -export interface StorefrontContext { /** * TODO: Add TSDoc */ - graphql: GraphQLClient; + storefront: StorefrontContext; } -export type GetStorefrontContext = (shop: string) => Promise; +export type GetUnauthenticatedStorefrontContext = ( + shop: string, +) => Promise; From 188e29b0971dc0bdc2058601ef45be5603e6098a Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:07:38 -0400 Subject: [PATCH 04/14] Restore private app SFAPI token behaviour --- .../__test-helpers/expect-admin-api-client.ts | 2 +- .../src/server/authenticate/admin/authenticate.ts | 8 ++------ .../public/appProxy/__tests__/authenticate.test.ts | 1 + .../src/server/clients/admin/factory.ts | 2 +- .../shopify-app-remix/src/server/config-types.ts | 6 ------ .../shopify-app-remix/src/server/shopify-app.ts | 14 -------------- 6 files changed, 5 insertions(+), 28 deletions(-) diff --git a/packages/shopify-app-remix/src/server/__test-helpers/expect-admin-api-client.ts b/packages/shopify-app-remix/src/server/__test-helpers/expect-admin-api-client.ts index 3957717153..2c83ad614c 100644 --- a/packages/shopify-app-remix/src/server/__test-helpers/expect-admin-api-client.ts +++ b/packages/shopify-app-remix/src/server/__test-helpers/expect-admin-api-client.ts @@ -1,7 +1,7 @@ import {Session} from '@shopify/shopify-api'; import {LATEST_API_VERSION} from '..'; -import {AdminApiContext} from '../config-types'; +import type {AdminApiContext} from '../clients'; import {mockExternalRequest} from './request-mock'; import {TEST_SHOP} from './const'; diff --git a/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.ts b/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.ts index 11908b38ae..6ec2a414c2 100644 --- a/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.ts +++ b/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.ts @@ -11,13 +11,9 @@ import { ShopifyRestResources, } from '@shopify/shopify-api'; -import {adminClientFactory} from '../../clients/admin'; +import {AdminApiContext, adminClientFactory} from '../../clients/admin'; import type {BasicParams} from '../../types'; -import type { - AdminApiContext, - AppConfig, - AppConfigArg, -} from '../../config-types'; +import type {AppConfig, AppConfigArg} from '../../config-types'; import { getSessionTokenHeader, validateSessionToken, diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts index 34ad98f7bb..1045eabef8 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts @@ -234,6 +234,7 @@ describe('authenticating app proxy requests', () => { expect(context).toStrictEqual({ session: undefined, admin: undefined, + storefront: undefined, liquid: expect.any(Function), }); }); diff --git a/packages/shopify-app-remix/src/server/clients/admin/factory.ts b/packages/shopify-app-remix/src/server/clients/admin/factory.ts index bdb4311361..16de59eac9 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/factory.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/factory.ts @@ -1,10 +1,10 @@ import {Session, ShopifyRestResources} from '@shopify/shopify-api'; -import {AdminApiContext} from '../../config-types'; import {BasicParams} from '../../types'; import {graphqlClientFactory} from './graphql'; import {restClientFactory} from './rest'; +import type {AdminApiContext} from './types'; interface RestClientOptions { params: BasicParams; diff --git a/packages/shopify-app-remix/src/server/config-types.ts b/packages/shopify-app-remix/src/server/config-types.ts index 5e076211ce..b66617dd14 100644 --- a/packages/shopify-app-remix/src/server/config-types.ts +++ b/packages/shopify-app-remix/src/server/config-types.ts @@ -211,12 +211,6 @@ export interface AppConfigArg< * ``` */ authPathPrefix?: string; - - /** - * TODO: Document - * - */ - privateStorefrontAccessToken?: string; } export interface AppConfig diff --git a/packages/shopify-app-remix/src/server/shopify-app.ts b/packages/shopify-app-remix/src/server/shopify-app.ts index 0595c721d0..195e26fa2b 100644 --- a/packages/shopify-app-remix/src/server/shopify-app.ts +++ b/packages/shopify-app-remix/src/server/shopify-app.ts @@ -68,13 +68,6 @@ export function shopifyApp< const params: BasicParams = {api, config, logger}; const oauth = new AuthStrategy(params); - if (appConfig.privateAppStorefrontAccessToken) { - logger.deprecated( - '2.0.0', - 'privateAppStorefrontAccessToken is deprecated. Use privateStorefrontAccessToken instead.', - ); - } - const shopify: | AdminApp | AppStoreApp @@ -170,16 +163,9 @@ function deriveConfig( const authPathPrefix = appConfig.authPathPrefix || '/auth'; appConfig.distribution = appConfig.distribution ?? AppDistribution.AppStore; - const privateAppStorefrontAccessToken = - appConfig.privateStorefrontAccessToken || - appConfig.privateAppStorefrontAccessToken; - - delete appConfig.privateStorefrontAccessToken; - return { ...appConfig, ...apiConfig, - privateAppStorefrontAccessToken, canUseLoginForm: appConfig.distribution !== AppDistribution.ShopifyAdmin, useOnlineTokens: appConfig.useOnlineTokens ?? false, hooks: appConfig.hooks ?? {}, From 11c85df7cd6c8a7cc451c4ab318bd474e2150578 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 16:13:01 -0400 Subject: [PATCH 05/14] Complete TSDOC comments for the new storefront client --- .../authenticate/public/appProxy/types.ts | 3 +- .../src/server/clients/admin/types.ts | 29 ++++-- .../src/server/clients/storefront/types.ts | 31 ++++++ .../src/server/config-types.ts | 95 +------------------ .../shopify-app-remix/src/server/types.ts | 54 +---------- .../unauthenticated/storefront/types.ts | 23 ++++- .../src/server/unauthenticated/types.ts | 68 +++++++++++++ 7 files changed, 144 insertions(+), 159 deletions(-) create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/types.ts diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts index e55e62e37f..f3c2863f03 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts @@ -119,8 +119,7 @@ export interface AppProxyContextWithSession< admin: AdminApiContext; /** - * TODO - * + * Method for interacting with the Shopify Storefront Graphql API for the store that made the request */ storefront: StorefrontContext; } diff --git a/packages/shopify-app-remix/src/server/clients/admin/types.ts b/packages/shopify-app-remix/src/server/clients/admin/types.ts index c442a67018..448a1f036f 100644 --- a/packages/shopify-app-remix/src/server/clients/admin/types.ts +++ b/packages/shopify-app-remix/src/server/clients/admin/types.ts @@ -23,15 +23,17 @@ export interface AdminApiContext< /** * Methods for interacting with the Shopify Admin REST API * - * There are methods for interacting with individual REST resources. You can also make plain `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. + * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. * * {@link https://shopify.dev/docs/api/admin-rest} * * @example - * Getting the number of orders in a store using rest resources + * Using REST resources. + * Getting the number of orders in a store using REST resources. + * * ```ts - * // app/shopify.server.ts - * import { shopifyApp } from "@shopify/shopify-app-remix"; + * // /app/shopify.server.ts + * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; * * const shopify = shopifyApp({ @@ -40,8 +42,10 @@ export interface AdminApiContext< * }); * export default shopify; * export const authenticate = shopify.authenticate; + * ``` * - * // app/routes/**\/.ts + * ```ts + * // /app/routes/**\/*.ts * import { LoaderArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * @@ -52,10 +56,12 @@ export interface AdminApiContext< * ``` * * @example - * Making a GET request to the REST API + * Performing a GET request to the REST API. + * Use `admin.rest.` to make custom requests to the API. + * * ```ts - * // app/shopify.server.ts - * import { shopifyApp } from "@shopify/shopify-app-remix"; + * // /app/shopify.server.ts + * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ @@ -64,8 +70,10 @@ export interface AdminApiContext< * }); * export default shopify; * export const authenticate = shopify.authenticate; + * ``` * - * // app/routes/**\/.ts + * ```ts + * // /app/routes/**\/*.ts * import { LoaderArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * @@ -86,7 +94,8 @@ export interface AdminApiContext< * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md} * * @example - * Creating a new product + * Querying the GraphQL API. + * Use `admin.graphql` to make query / mutation requests. * ```ts * import { ActionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; diff --git a/packages/shopify-app-remix/src/server/clients/storefront/types.ts b/packages/shopify-app-remix/src/server/clients/storefront/types.ts index 00d70f630d..724792ad59 100644 --- a/packages/shopify-app-remix/src/server/clients/storefront/types.ts +++ b/packages/shopify-app-remix/src/server/clients/storefront/types.ts @@ -1,5 +1,36 @@ import {GraphQLClient} from '../types'; export interface StorefrontContext { + /** + * Method for interacting with the Shopify Storefront GraphQL API + * + * {@link https://shopify.dev/docs/api/storefront} + * + * @example + * Getting a list of blog posts a new product + * ```ts + * import { ActionArgs } from "@remix-run/node"; + * import { authenticate } from "../shopify.server"; + * + * export async function action({ request }: ActionArgs) { + * const { admin } = await authenticate.admin(request); + * + * const response = await admin.graphql( + * `#graphql + * mutation populateProduct($input: ProductInput!) { + * productCreate(input: $input) { + * product { + * id + * } + * } + * }`, + * { variables: { input: { title: "Product Name" } } } + * ); + * + * const productData = await response.json(); + * return json({ data: productData.data }); + * } + * ``` + */ graphql: GraphQLClient; } diff --git a/packages/shopify-app-remix/src/server/config-types.ts b/packages/shopify-app-remix/src/server/config-types.ts index b66617dd14..41eb19aa2d 100644 --- a/packages/shopify-app-remix/src/server/config-types.ts +++ b/packages/shopify-app-remix/src/server/config-types.ts @@ -8,7 +8,7 @@ import { } from '@shopify/shopify-api'; import {SessionStorage} from '@shopify/shopify-app-session-storage'; -import {AppDistribution} from './types'; +import type {AppDistribution} from './types'; import type {AdminApiContext} from './clients'; export interface AppConfigArg< @@ -275,96 +275,3 @@ export interface AfterAuthOptions< session: Session; admin: AdminApiContext; } - -export interface AdminApiContext< - Resources extends ShopifyRestResources = ShopifyRestResources, -> { - /** - * Methods for interacting with the REST Admin API. - * - * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. - * - * {@link https://shopify.dev/docs/api/admin-rest} - * - * @example - * Using REST resources. - * Getting the number of orders in a store using REST resources. - * - * ```ts - * // /app/shopify.server.ts - * import { shopifyApp } from "@shopify/shopify-app-remix/server"; - * import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; - * - * const shopify = shopifyApp({ - * restResources, - * // ...etc - * }); - * export default shopify; - * export const authenticate = shopify.authenticate; - * ``` - * - * ```ts - * // /app/routes/**\/*.ts - * import { LoaderArgs, json } from "@remix-run/node"; - * import { authenticate } from "../shopify.server"; - * - * export const loader = async ({ request }: LoaderArgs) => { - * const { admin, session } = await authenticate.admin(request); - * return json(admin.rest.resources.Order.count({ session })); - * }; - * ``` - * - * @example - * Performing a GET request to the REST API. - * Use `admin.rest.` to make custom requests to the API. - * - * ```ts - * // /app/routes/**\/*.ts - * import { LoaderArgs, json } from "@remix-run/node"; - * import { authenticate } from "../shopify.server"; - * - * export const loader = async ({ request }: LoaderArgs) => { - * const { admin, session } = await authenticate.admin(request); - * const response = await admin.rest.get({ path: "/customers/count.json" }); - * const customers = await response.json(); - * return json({ customers }); - * }; - * ``` - */ - rest: RestClientWithResources; - - /** - * Methods for interacting with the GraphQL Admin API. - * - * {@link https://shopify.dev/docs/api/admin-graphql} - * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md} - * - * @example - * Querying the GraphQL API. - * Use `admin.graphql` to make query / mutation requests. - * ```ts - * import { ActionArgs } from "@remix-run/node"; - * import { authenticate } from "../shopify.server"; - * - * export async function action({ request }: ActionArgs) { - * const { admin } = await authenticate.admin(request); - * - * const response = await admin.graphql( - * `#graphql - * mutation populateProduct($input: ProductInput!) { - * productCreate(input: $input) { - * product { - * id - * } - * } - * }`, - * { variables: { input: { title: "Product Name" } } } - * ); - * - * const productData = await response.json(); - * return json({ data: productData.data }); - * } - * ``` - */ - graphql: GraphQLClient; -} diff --git a/packages/shopify-app-remix/src/server/types.ts b/packages/shopify-app-remix/src/server/types.ts index 3f0d745a04..9acba94c3d 100644 --- a/packages/shopify-app-remix/src/server/types.ts +++ b/packages/shopify-app-remix/src/server/types.ts @@ -11,6 +11,8 @@ import type { RegisterWebhooksOptions, } from './authenticate/webhooks/types'; import type {AuthenticatePublic} from './authenticate/public/types'; +import type {AdminContext} from './authenticate/admin/types'; +import type {Unauthenticated} from './unauthenticated/types'; export interface BasicParams { api: Shopify; @@ -67,15 +69,6 @@ type AuthenticateAdmin< Resources extends ShopifyRestResources = ShopifyRestResources, > = (request: Request) => Promise>; -type AuthenticateWebhook< - Resources extends ShopifyRestResources = ShopifyRestResources, - Topics = string | number | symbol, -> = ( - request: Request, -) => Promise< - WebhookContext | WebhookContextWithSession ->; - type RestResourcesType = Config['restResources'] extends ShopifyRestResources ? Config['restResources'] @@ -202,47 +195,6 @@ interface Authenticate { >; } -type UnauthenticatedAdmin = ( - shop: string, -) => Promise>; - -interface Unauthenticated { - /** - * Get an admin context by passing a shop - * - * **Warning** This should only be used for Requests that do not originate from Shopify. - * You must do your own authentication before using this method. - * This method throws an error if there is no session for the shop. - * - * @example - * Responding to a request from an external service not controlled by Shopify. - * ```ts - * // app/shopify.server.ts - * import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix"; - * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; - * - * const shopify = shopifyApp({ - * restResources, - * // ...etc - * }); - * export default shopify; - * - * // app/routes/**\/*.jsx - * import { LoaderArgs, json } from "@remix-run/node"; - * import { authenticateExternal } from "~/helpers/authenticate" - * import shopify from "../../shopify.server"; - * - * export async function loader({ request }: LoaderArgs) { - * const shop = await authenticateExternal(request) - * const {admin} = await shopify.unauthenticated.admin(shop); - * - * return json(await admin.rest.resources.Product.count({ session })); - * } - * ``` - */ - admin: UnauthenticatedAdmin>; -} - export interface ShopifyAppBase { /** * The `SessionStorage` instance you passed in as a config option. @@ -401,7 +353,7 @@ export interface ShopifyAppBase { * } * ``` */ - unauthenticated: Unauthenticated; + unauthenticated: Unauthenticated>; } interface ShopifyAppLogin { diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts index ee5afe219c..50add0786f 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts @@ -4,12 +4,31 @@ import type {StorefrontContext} from '../../clients'; export interface UnauthenticatedStorefrontContext { /** - * TODO: Add TSDoc + * The session for the given shop. + * + * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. + * + * This will always be an offline session. You can use this to get shop specific data. + * + * @example + * Getting shop specific data using a session + * ```ts + * // app/routes/**\/.ts + * import { LoaderArgs, json } from "@remix-run/node"; + * import { unauthenticated } from "../shopify.server"; + * import { getWidgets } from "~/db/widgets.server"; + * + * export const loader = async ({ request }: LoaderArgs) => { + * const shop = getShopFromExternalRequest(request); + * const { session } = await unauthenticated.storefront(shop); + * return json(await getWidgets({shop: session.shop)); + * }; + * ``` */ session: Session; /** - * TODO: Add TSDoc + * Method for interacting with the Shopify GraphQL Storefront API for the given store. */ storefront: StorefrontContext; } diff --git a/packages/shopify-app-remix/src/server/unauthenticated/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/types.ts new file mode 100644 index 0000000000..d8b3dbbfc3 --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/types.ts @@ -0,0 +1,68 @@ +import type {ShopifyRestResources} from '@shopify/shopify-api'; + +import type {GetUnauthenticatedAdminContext} from './admin/types'; +import type {GetUnauthenticatedStorefrontContext} from './storefront/types'; + +export interface Unauthenticated { + /** + * Get an admin context by passing a shop + * + * **Warning** This should only be used for Requests that do not originate from Shopify. + * You must do your own authentication before using this method. + * This method throws an error if there is no session for the shop. + * + * @example + * Responding to a request not controlled by Shopify. + * ```ts + * // /app/shopify.server.ts + * import { LATEST_API_VERSION, shopifyApp } from "@shopify/shopify-app-remix/server"; + * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; + * + * const shopify = shopifyApp({ + * restResources, + * // ...etc + * }); + * export default shopify; + * ``` + * ```ts + * // /app/routes/**\/*.jsx + * import { LoaderArgs, json } from "@remix-run/node"; + * import { authenticateExternal } from "~/helpers/authenticate" + * import shopify from "../../shopify.server"; + * + * export async function loader({ request }: LoaderArgs) { + * const shop = await authenticateExternal(request) + * const {admin} = await shopify.unauthenticated.admin(shop); + * + * return json(await admin.rest.resources.Product.count({ session })); + * } + * ``` + */ + admin: GetUnauthenticatedAdminContext; + + /** + * Get a storefront context by passing a shop + * + * **Warning** This should only be used for Requests that do not originate from Shopify. + * You must do your own authentication before using this method. + * This method throws an error if there is no session for the shop. + * + * @example + * Responding to a request not controlled by Shopify + * ```ts + * // /app/routes/**\/*.jsx + * import { LoaderArgs, json } from "@remix-run/node"; + * import { authenticateExternal } from "~/helpers/authenticate" + * import shopify from "../../shopify.server"; + * + * export async function loader({ request }: LoaderArgs) { + * const shop = await authenticateExternal(request) + * const {storefront} = await shopify.unauthenticated.storefront(shop); + * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`) + * + * return json(await response.json()); + * } + * ``` + */ + storefront: GetUnauthenticatedStorefrontContext; +} From fd1e759e5e5fd2231ddd95eb6d05cf2c1e2eb687 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 16:31:55 -0400 Subject: [PATCH 06/14] Abstract getting a valid App Proxy Request and update the AppProxy Admin client to test the App Proxy, not unauthenticated.admin (oops) --- .../appProxy/__tests__/authenticate.test.ts | 58 +++++++------------ 1 file changed, 21 insertions(+), 37 deletions(-) diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts index 1045eabef8..843059d7db 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts @@ -98,13 +98,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const {liquid} = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); const response = liquid('Liquid template {{shop.name}}'); @@ -120,13 +115,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const {liquid} = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); const response = liquid('Liquid template {{shop.name}}', 400); @@ -140,13 +130,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const {liquid} = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); const response = liquid('Liquid template {{shop.name}}', { headers: { @@ -169,13 +154,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const {liquid} = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); const response = liquid('Liquid template {{shop.name}}', { layout: false, @@ -193,13 +173,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const {liquid} = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); const response = liquid('Liquid template {{shop.name}}', { status: 400, @@ -221,13 +196,8 @@ describe('authenticating app proxy requests', () => { const shopify = shopifyApp(config); // WHEN - const url = new URL(APP_URL); - url.searchParams.set('shop', TEST_SHOP); - url.searchParams.set('timestamp', secondsInPast(1)); - url.searchParams.set('signature', await createAppProxyHmac(url)); - const context = await shopify.authenticate.public.appProxy( - new Request(url.toString()), + await getValidRequest(), ); // THEN @@ -247,14 +217,28 @@ describe('authenticating app proxy requests', () => { shopify.sessionStorage, false, ); + const {admin, session: actualSession} = - await shopify.unauthenticated.admin(TEST_SHOP); + await shopify.authenticate.public.appProxy(await getValidRequest()); + + if (!admin) { + throw new Error('No admin client'); + } return {admin, expectedSession, actualSession}; }); }); }); +async function getValidRequest(): Promise { + const url = new URL(APP_URL); + url.searchParams.set('shop', TEST_SHOP); + url.searchParams.set('timestamp', secondsInPast(1)); + url.searchParams.set('signature', await createAppProxyHmac(url)); + + return new Request(url.toString()); +} + async function createAppProxyHmac(url: URL): Promise { const params = Object.fromEntries(url.searchParams.entries()); const string = Object.entries(params) From 06090fc6ba2eb0e7f08ad0431c98df0988a486f1 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 17:07:53 -0400 Subject: [PATCH 07/14] Added test to authenticate appProxy to check we return a storefront client --- .../appProxy/__tests__/authenticate.test.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts index 843059d7db..be380df4c4 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts @@ -1,12 +1,13 @@ import {HashFormat, createSHA256HMAC} from '@shopify/shopify-api/runtime'; -import {shopifyApp} from '../../../..'; +import {LATEST_API_VERSION, shopifyApp} from '../../../..'; import { API_SECRET_KEY, APP_URL, TEST_SHOP, expectAdminApiClient, getThrownResponse, + mockExternalRequest, setUpValidSession, testConfig, } from '../../../../__test-helpers'; @@ -228,6 +229,43 @@ describe('authenticating app proxy requests', () => { return {admin, expectedSession, actualSession}; }); }); + + describe('Valid requests with a session return a Storefront API client', () => { + it('Can perform GraphQL Requests', async () => { + // GIVEN + const shopify = shopifyApp(testConfig()); + const session = await setUpValidSession(shopify.sessionStorage, false); + const apiResponse = {blogs: {nodes: [{id: 1}]}}; + + await mockExternalRequest({ + request: new Request( + `https://${TEST_SHOP}/api/${LATEST_API_VERSION}/graphql.json`, + { + method: 'POST', + headers: {'Shopify-Storefront-Private-Token': session.accessToken!}, + }, + ), + response: new Response(JSON.stringify(apiResponse)), + }); + + // WHEN + const {storefront} = await shopify.authenticate.public.appProxy( + await getValidRequest(), + ); + + if (!storefront) { + throw new Error('No storefront client'); + } + + const response = await storefront.graphql( + 'blogs(first: 1) { nodes { id }}', + ); + + // THEN + expect(response.status).toEqual(200); + expect(await response.json()).toEqual(apiResponse); + }); + }); }); async function getValidRequest(): Promise { From 945d7db4710460fcd384e1337bc21ae07d13f31f Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 17:22:42 -0400 Subject: [PATCH 08/14] Add new test helper to test storefront API client. Use it for authenticate.appProxy and unauthenticated.storefront --- .../expect-storefront-api-client.ts | 50 +++++++++++++++++++ .../src/server/__test-helpers/index.ts | 1 + .../appProxy/__tests__/authenticate.test.ts | 35 ++++--------- .../storefront/__tests__/factory.test.ts | 21 ++++++++ 4 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 packages/shopify-app-remix/src/server/__test-helpers/expect-storefront-api-client.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts diff --git a/packages/shopify-app-remix/src/server/__test-helpers/expect-storefront-api-client.ts b/packages/shopify-app-remix/src/server/__test-helpers/expect-storefront-api-client.ts new file mode 100644 index 0000000000..bf6a4dd3e4 --- /dev/null +++ b/packages/shopify-app-remix/src/server/__test-helpers/expect-storefront-api-client.ts @@ -0,0 +1,50 @@ +import {Session} from '@shopify/shopify-api'; + +import {LATEST_API_VERSION} from '..'; +import type {StorefrontContext} from '../clients'; + +import {mockExternalRequest} from './request-mock'; +import {TEST_SHOP} from './const'; + +export function expectStorefrontApiClient( + factory: () => Promise<{ + storefront: StorefrontContext; + expectedSession: Session; + actualSession: Session; + }>, +) { + it('Storefront client can perform GraphQL Requests', async () => { + // GIVEN + const {storefront, actualSession} = await factory(); + const apiResponse = {blogs: {nodes: [{id: 1}]}}; + await mockExternalRequest({ + request: new Request( + `https://${TEST_SHOP}/api/${LATEST_API_VERSION}/graphql.json`, + { + method: 'POST', + headers: { + 'Shopify-Storefront-Private-Token': actualSession.accessToken!, + }, + }, + ), + response: new Response(JSON.stringify(apiResponse)), + }); + + // WHEN + const response = await storefront.graphql( + 'blogs(first: 1) { nodes { id }}', + ); + + // THEN + expect(response.status).toEqual(200); + expect(await response.json()).toEqual(apiResponse); + }); + + it('Storefront client uses the correct session', async () => { + // GIVEN + const {expectedSession, actualSession} = await factory(); + + // THEN + expect(expectedSession).toEqual(actualSession); + }); +} diff --git a/packages/shopify-app-remix/src/server/__test-helpers/index.ts b/packages/shopify-app-remix/src/server/__test-helpers/index.ts index df414036ce..fe70b4c802 100644 --- a/packages/shopify-app-remix/src/server/__test-helpers/index.ts +++ b/packages/shopify-app-remix/src/server/__test-helpers/index.ts @@ -7,6 +7,7 @@ export * from './expect-begin-auth-redirect'; export * from './expect-document-request-headers'; export * from './expect-exit-iframe'; export * from './expect-login-redirect'; +export * from './expect-storefront-api-client'; export * from './get-hmac'; export * from './get-jwt'; export * from './get-thrown-response'; diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts index be380df4c4..81eb4bc146 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/__tests__/authenticate.test.ts @@ -6,6 +6,7 @@ import { APP_URL, TEST_SHOP, expectAdminApiClient, + expectStorefrontApiClient, getThrownResponse, mockExternalRequest, setUpValidSession, @@ -231,39 +232,21 @@ describe('authenticating app proxy requests', () => { }); describe('Valid requests with a session return a Storefront API client', () => { - it('Can perform GraphQL Requests', async () => { - // GIVEN + expectStorefrontApiClient(async () => { const shopify = shopifyApp(testConfig()); - const session = await setUpValidSession(shopify.sessionStorage, false); - const apiResponse = {blogs: {nodes: [{id: 1}]}}; - - await mockExternalRequest({ - request: new Request( - `https://${TEST_SHOP}/api/${LATEST_API_VERSION}/graphql.json`, - { - method: 'POST', - headers: {'Shopify-Storefront-Private-Token': session.accessToken!}, - }, - ), - response: new Response(JSON.stringify(apiResponse)), - }); - - // WHEN - const {storefront} = await shopify.authenticate.public.appProxy( - await getValidRequest(), + const expectedSession = await setUpValidSession( + shopify.sessionStorage, + false, ); + const {storefront, session: actualSession} = + await shopify.authenticate.public.appProxy(await getValidRequest()); + if (!storefront) { throw new Error('No storefront client'); } - const response = await storefront.graphql( - 'blogs(first: 1) { nodes { id }}', - ); - - // THEN - expect(response.status).toEqual(200); - expect(await response.json()).toEqual(apiResponse); + return {storefront, expectedSession, actualSession}; }); }); }); diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts new file mode 100644 index 0000000000..cdede068e6 --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts @@ -0,0 +1,21 @@ +import {shopifyApp} from '../../../index'; +import { + TEST_SHOP, + setUpValidSession, + testConfig, + expectStorefrontApiClient, +} from '../../../__test-helpers'; + +describe('unauthenticated storefront context', () => { + expectStorefrontApiClient(async () => { + const shopify = shopifyApp(testConfig()); + const expectedSession = await setUpValidSession( + shopify.sessionStorage, + false, + ); + const {storefront, session: actualSession} = + await shopify.unauthenticated.storefront(TEST_SHOP); + + return {storefront, expectedSession, actualSession}; + }); +}); From 1cd3f6380f0d9160ada3b8ea168930d59409072e Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 17:23:48 -0400 Subject: [PATCH 09/14] Remove some code that should have been removed in a previous PR --- .../server/unauthenticated/admin/__tests__/factory.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts b/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts index 5b1d888e1a..6235742842 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts @@ -1,5 +1,3 @@ -import {LATEST_API_VERSION} from '@shopify/shopify-api'; - import {shopifyApp} from '../../../index'; import { TEST_SHOP, @@ -8,8 +6,6 @@ import { expectAdminApiClient, } from '../../../__test-helpers'; -const REQUEST_URL = `https://${TEST_SHOP}/admin/api/${LATEST_API_VERSION}/customers.json`; - describe('unauthenticated admin context', () => { expectAdminApiClient(async () => { const shopify = shopifyApp(testConfig()); From efd4af6e7d55b121f63402d1b26deecdc3499e04 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Wed, 13 Sep 2023 17:37:25 -0400 Subject: [PATCH 10/14] Added tests to verify we throw an error when there is no session for the shop passed to unauthenticated.dmin or unauthenticated.storefront --- .../unauthenticated/admin/__tests__/factory.test.ts | 8 ++++++++ .../storefront/__tests__/factory.test.ts | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts b/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts index 6235742842..7e74ec6280 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/admin/__tests__/factory.test.ts @@ -7,6 +7,14 @@ import { } from '../../../__test-helpers'; describe('unauthenticated admin context', () => { + it('throws an error if there is no offline session for the shop', async () => { + // GIVEN + const shopify = shopifyApp(testConfig()); + + // EXPECT + await expect(shopify.unauthenticated.admin(TEST_SHOP)).rejects.toThrow(); + }); + expectAdminApiClient(async () => { const shopify = shopifyApp(testConfig()); const expectedSession = await setUpValidSession( diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts index cdede068e6..5ac01e34e6 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/__tests__/factory.test.ts @@ -7,6 +7,16 @@ import { } from '../../../__test-helpers'; describe('unauthenticated storefront context', () => { + it('throws an error if there is no offline session for the shop', async () => { + // GIVEN + const shopify = shopifyApp(testConfig()); + + // EXPECT + await expect( + shopify.unauthenticated.storefront(TEST_SHOP), + ).rejects.toThrow(); + }); + expectStorefrontApiClient(async () => { const shopify = shopifyApp(testConfig()); const expectedSession = await setUpValidSession( From c566dedccd841a1ada75f7241b33241efa4c8cfb Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:35:01 -0400 Subject: [PATCH 11/14] Updating docs for SFAPI client --- .../docs/generated/generated_docs_data.json | 2971 ++++++++--------- .../authenticate.admin.billing.doc.ts | 0 .../authenticate.public.app-proxy.doc.ts | 12 +- .../admin/authenticate.admin.api.doc.ts | 0 .../authenticate.storefront.api.doc.ts | 34 + .../src/server/clients/storefront/types.ts | 19 +- .../admin/unauthenticated.admin.doc.ts | 2 +- .../unauthenticated/storefront/types.ts | 7 +- .../unauthenticated.storefront.doc.ts | 30 + 9 files changed, 1448 insertions(+), 1627 deletions(-) rename packages/shopify-app-remix/src/server/authenticate/admin/{ => billing}/authenticate.admin.billing.doc.ts (100%) rename packages/shopify-app-remix/src/server/{authenticate => clients}/admin/authenticate.admin.api.doc.ts (100%) create mode 100644 packages/shopify-app-remix/src/server/clients/storefront/authenticate.storefront.api.doc.ts create mode 100644 packages/shopify-app-remix/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts diff --git a/packages/shopify-app-remix/docs/generated/generated_docs_data.json b/packages/shopify-app-remix/docs/generated/generated_docs_data.json index 24dc0bf18c..0b24b2d1c7 100644 --- a/packages/shopify-app-remix/docs/generated/generated_docs_data.json +++ b/packages/shopify-app-remix/docs/generated/generated_docs_data.json @@ -120,28 +120,133 @@ } }, { - "name": "Admin API", - "description": "Contains objects used to interact with the Admin API.\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).", - "category": "APIs", + "name": "Admin", + "description": "Contains functions for authenticating and interacting with the Admin API.\n\nThis function can handle requests for apps embedded in the Admin, Admin extensions, or non-embedded apps.", + "category": "Authenticate", "type": "object", "isVisualComponent": false, "definitions": [ { - "title": "admin", - "description": "Provides utilities that apps can use to make requests to the Admin API.", - "type": "AdminApiContext", + "title": "authenticate.admin", + "description": "Authenticates requests coming from the Shopify admin.\n\nThe shape of the returned object changes depending on the `isEmbeddedApp` config.", + "type": "AuthenticateAdmin", "typeDefinitions": { + "AuthenticateAdmin": { + "filePath": "/server/authenticate/admin/types.ts", + "name": "AuthenticateAdmin", + "description": "", + "params": [ + { + "name": "request", + "description": "", + "value": "Request", + "filePath": "/server/authenticate/admin/types.ts" + } + ], + "returns": { + "filePath": "/server/authenticate/admin/types.ts", + "description": "", + "name": "Promise>", + "value": "Promise>" + }, + "value": "export type AuthenticateAdmin<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> = (request: Request) => Promise>;" + }, + "AdminContext": { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "AdminContext", + "value": "Config['isEmbeddedApp'] extends false\n ? NonEmbeddedAdminContext\n : EmbeddedAdminContext", + "description": "" + }, + "NonEmbeddedAdminContext": { + "filePath": "/server/authenticate/admin/types.ts", + "name": "NonEmbeddedAdminContext", + "description": "", + "members": [ + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "session", + "value": "Session", + "description": "The session for the user who made the request.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nUse this to get shop or user-specific data.", + "examples": [ + { + "title": "Using offline sessions", + "description": "Get your app's shop-specific data using an offline session.", + "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", + "title": "/app/routes/**\\/*.ts" + } + ] + }, + { + "title": "Using online sessions", + "description": "Get your app's user-specific data using an online session.", + "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", + "title": "/app/routes/**\\/*.ts" + } + ] + } + ] + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "admin", + "value": "AdminApiContext", + "description": "Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request." + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "billing", + "value": "BillingContext", + "description": "Billing methods for this store, based on the plans defined in the `billing` config option.\n\n\n\n\n" + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "cors", + "value": "EnsureCORSFunction", + "description": "A function that ensures the CORS headers are set correctly for the response.", + "examples": [ + { + "title": "Setting CORS headers for a admin request", + "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", + "tabs": [ + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", + "title": "/app/routes/admin/my-route.ts" + } + ] + } + ] + } + ], + "value": "export interface NonEmbeddedAdminContext<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends AdminContextInternal {}" + }, "AdminApiContext": { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "name": "AdminApiContext", "description": "", "members": [ { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "rest", "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", + "description": "Methods for interacting with the Shopify Admin REST API\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", "examples": [ { "title": "Using REST resources", @@ -161,6 +266,10 @@ "title": "Performing a GET request to the REST API", "description": "Use `admin.rest.` to make custom requests to the API.", "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, { "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", "title": "/app/routes/**\\/*.ts" @@ -170,11 +279,11 @@ ] }, { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", + "value": "GraphQLClient", + "description": "Methods for interacting with the Shopify Admin GraphQL API\n\n\n\n\n\n\n\n\n\n", "examples": [ { "title": "Querying the GraphQL API", @@ -189,7 +298,7 @@ ] } ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" + "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the Shopify Admin REST API\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the Shopify Admin GraphQL API\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" }, "RestClientWithResources": { "filePath": "/server/clients/admin/rest.ts", @@ -198,393 +307,455 @@ "value": "RemixRestClient & {resources: Resources}", "description": "" }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", + "BillingContext": { + "filePath": "/server/authenticate/admin/billing/types.ts", + "name": "BillingContext", "description": "", - "params": [ + "members": [ { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "require", + "value": "(options: RequireBillingOptions) => Promise", + "description": "Checks if the shop has an active payment for any plan defined in the `billing` config option.", + "examples": [ + { + "title": "Requesting billing right away", + "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", + "tabs": [ + { + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", + "title": "/app/routes/**\\/*.ts" + }, + { + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + } + ] + }, + { + "title": "Using a plan selection page", + "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", + "tabs": [ + { + "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", + "title": "/app/routes/**\\/*.ts" + }, + { + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + } + ] + } + ] }, { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "request", + "value": "(options: RequestBillingOptions) => Promise", + "description": "Requests payment for the plan.", + "examples": [ + { + "title": "Using a custom return URL", + "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", + "tabs": [ + { + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", + "title": "/app/routes/**\\/*.ts" + }, + { + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + } + ] + } + ] + }, + { + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "cancel", + "value": "(options: CancelBillingOptions) => Promise", + "description": "Cancels an ongoing subscription, given its ID.", + "examples": [ + { + "title": "Cancelling a subscription", + "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", + "tabs": [ + { + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", + "title": "/app/routes/cancel-subscription.ts" + }, + { + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" + } + ] + } + ] } ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" + "value": "export interface BillingContext {\n /**\n * Checks if the shop has an active payment for any plan defined in the `billing` config option.\n *\n * @returns A promise that resolves to an object containing the active purchases for the shop.\n *\n * @example\n * Requesting billing right away.\n * Call `billing.request` in the `onFailure` callback to immediately request payment.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * isTest: true,\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * @example\n * Using a plan selection page.\n * Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, redirect } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n * isTest: true,\n * onFailure: () => redirect('/select-plan'),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n require: (\n options: RequireBillingOptions,\n ) => Promise;\n\n /**\n * Requests payment for the plan.\n *\n * @returns Redirects to the confirmation URL for the payment.\n *\n * @example\n * Using a custom return URL.\n * Change where the merchant is returned to after approving the purchase using the `returnUrl` option.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({\n * plan: MONTHLY_PLAN,\n * isTest: true,\n * returnUrl: '/billing-complete',\n * }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n request: (options: RequestBillingOptions) => Promise;\n\n /**\n * Cancels an ongoing subscription, given its ID.\n *\n * @returns The cancelled subscription.\n *\n * @example\n * Cancelling a subscription.\n * Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.\n * ```ts\n * // /app/routes/cancel-subscription.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * const cancelledSubscription = await billing.cancel({\n * subscriptionId: subscription.id,\n * isTest: true,\n * prorate: true,\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n cancel: (options: CancelBillingOptions) => Promise;\n}" }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", + "RequireBillingOptions": { + "filePath": "/server/authenticate/admin/billing/types.ts", + "name": "RequireBillingOptions", "description": "", "members": [ { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true + "name": "plans", + "value": "(keyof Config[\"billing\"])[]", + "description": "The plans to check for. Must be one of the values defined in the `billing` config option." }, { - "filePath": "/server/clients/admin/graphql.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true + "name": "onFailure", + "value": "(error: any) => Promise", + "description": "How to handle the request if the shop doesn't have an active payment for any plan." }, { - "filePath": "/server/clients/admin/graphql.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", + "name": "isTest", + "value": "boolean", "description": "", "isOptional": true } ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" + "value": "export interface RequireBillingOptions\n extends Omit {\n /**\n * The plans to check for. Must be one of the values defined in the `billing` config option.\n */\n plans: (keyof Config['billing'])[];\n /**\n * How to handle the request if the shop doesn't have an active payment for any plan.\n */\n onFailure: (error: any) => Promise;\n}" }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", + "RequestBillingOptions": { + "filePath": "/server/authenticate/admin/billing/types.ts", + "name": "RequestBillingOptions", "description": "", "members": [ { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "plan", + "value": "keyof Config[\"billing\"]", + "description": "The plan to request. Must be one of the values defined in the `billing` config option." } ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" - } - } - } - ], - "jsDocTypeExamples": [ - "AdminApiContext" - ], - "related": [ - { - "name": "Authenticated context", - "subtitle": "Authenticate requests from Shopify Admin.", - "url": "/docs/api/shopify-app-remix/authenticate/admin" - }, - { - "name": "Unauthenticated context", - "subtitle": "Interact with the Admin API on non-Shopify requests.", - "url": "/docs/api/shopify-app-remix/unauthenticated/unauthenticated-admin" - } - ], - "examples": { - "description": "", - "exampleGroups": [ - { - "title": "rest", - "examples": [ - { - "description": "Getting the number of orders in a store using REST resources.", - "codeblock": { - "title": "Using REST resources", - "tabs": [ - { - "title": "/app/shopify.server.ts", - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "language": "typescript" - }, - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", - "language": "typescript" - } - ] - } - }, - { - "description": "Use `admin.rest.` to make custom requests to the API.", - "codeblock": { - "title": "Performing a GET request to the REST API", - "tabs": [ - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "graphql", - "examples": [ - { - "description": "Use `admin.graphql` to make query / mutation requests.", - "codeblock": { - "title": "Querying the GraphQL API", - "tabs": [ - { - "title": "Example", - "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "language": "typescript" - } - ] - } - } - ] - } - ] - } - }, - { - "name": "Billing", - "description": "Contains function used to bill merchants for your app.\n\nThis object is returned on authenticated Admin requests.", - "category": "APIs", - "type": "object", - "isVisualComponent": false, - "definitions": [ - { - "title": "billing", - "description": "Provides utilities that apps can use to request billing for the app using the Admin API.", - "type": "BillingContext", - "typeDefinitions": { - "BillingContext": { + "value": "export interface RequestBillingOptions\n extends Omit {\n /**\n * The plan to request. Must be one of the values defined in the `billing` config option.\n */\n plan: keyof Config['billing'];\n}" + }, + "CancelBillingOptions": { "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "BillingContext", + "name": "CancelBillingOptions", "description": "", "members": [ { "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "require", - "value": "(options: RequireBillingOptions) => Promise", - "description": "Checks if the shop has an active payment for any plan defined in the `billing` config option.", + "name": "subscriptionId", + "value": "string", + "description": "The ID of the subscription to cancel." + }, + { + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "prorate", + "value": "boolean", + "description": "Whether to prorate the cancellation.\n\n\n\n\n", + "isOptional": true + }, + { + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "isTest", + "value": "boolean", + "description": "", + "isOptional": true + } + ], + "value": "export interface CancelBillingOptions {\n /**\n * The ID of the subscription to cancel.\n */\n subscriptionId: string;\n /**\n * Whether to prorate the cancellation.\n *\n * {@link https://shopify.dev/docs/apps/billing/subscriptions/cancel-recurring-charges}\n */\n prorate?: boolean;\n isTest?: boolean;\n}" + }, + "EmbeddedAdminContext": { + "filePath": "/server/authenticate/admin/types.ts", + "name": "EmbeddedAdminContext", + "description": "", + "members": [ + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "sessionToken", + "value": "JwtPayload", + "description": "The decoded and validated session token for the request.\n\nReturned only if `isEmbeddedApp` is `true`.\n\n\n\n\n", "examples": [ { - "title": "Requesting billing right away", - "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", + "title": "Using the decoded session token", + "description": "Get user-specific data using the `sessionToken` object.", "tabs": [ { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" }, { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { sessionToken } = await authenticate.public.checkout(\n request\n );\n return json(await getMyAppData({user: sessionToken.sub}));\n};", + "title": "/app/routes/**\\/*.ts" } ] - }, + } + ] + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "redirect", + "value": "RedirectFunction", + "description": "A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded apps.\n\nReturned only if `isEmbeddedApp` is `true`.", + "examples": [ { - "title": "Using a plan selection page", - "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", + "title": "Redirecting to an app route", + "description": "Use the `redirect` helper to safely redirect between pages.", "tabs": [ { - "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" - }, + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\");\n};", + "title": "/app/routes/admin/my-route.ts" + } + ] + }, + { + "title": "Redirecting outside of Shopify admin", + "description": "Pass in a `target` option of `_top` or `_parent` to go to an external URL.", + "tabs": [ { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\", { target: '_parent' });\n};", + "title": "/app/routes/admin/my-route.ts" } ] } ] }, { - "filePath": "/server/authenticate/admin/billing/types.ts", + "filePath": "/server/authenticate/admin/types.ts", "syntaxKind": "PropertySignature", - "name": "request", - "value": "(options: RequestBillingOptions) => Promise", - "description": "Requests payment for the plan.", + "name": "session", + "value": "Session", + "description": "The session for the user who made the request.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nUse this to get shop or user-specific data.", "examples": [ { - "title": "Using a custom return URL", - "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", + "title": "Using offline sessions", + "description": "Get your app's shop-specific data using an offline session.", "tabs": [ { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" }, { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", + "title": "/app/routes/**\\/*.ts" + } + ] + }, + { + "title": "Using online sessions", + "description": "Get your app's user-specific data using an online session.", + "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", "title": "shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", + "title": "/app/routes/**\\/*.ts" } ] } ] }, { - "filePath": "/server/authenticate/admin/billing/types.ts", + "filePath": "/server/authenticate/admin/types.ts", "syntaxKind": "PropertySignature", - "name": "cancel", - "value": "(options: CancelBillingOptions) => Promise", - "description": "Cancels an ongoing subscription, given its ID.", + "name": "admin", + "value": "AdminApiContext", + "description": "Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request." + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "billing", + "value": "BillingContext", + "description": "Billing methods for this store, based on the plans defined in the `billing` config option.\n\n\n\n\n" + }, + { + "filePath": "/server/authenticate/admin/types.ts", + "syntaxKind": "PropertySignature", + "name": "cors", + "value": "EnsureCORSFunction", + "description": "A function that ensures the CORS headers are set correctly for the response.", "examples": [ { - "title": "Cancelling a subscription", - "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", + "title": "Setting CORS headers for a admin request", + "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", "tabs": [ { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", - "title": "/app/routes/cancel-subscription.ts" - }, - { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", + "title": "/app/routes/admin/my-route.ts" } ] } ] } ], - "value": "export interface BillingContext {\n /**\n * Checks if the shop has an active payment for any plan defined in the `billing` config option.\n *\n * @returns A promise that resolves to an object containing the active purchases for the shop.\n *\n * @example\n * Requesting billing right away.\n * Call `billing.request` in the `onFailure` callback to immediately request payment.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * isTest: true,\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * @example\n * Using a plan selection page.\n * Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, redirect } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n * isTest: true,\n * onFailure: () => redirect('/select-plan'),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n require: (\n options: RequireBillingOptions,\n ) => Promise;\n\n /**\n * Requests payment for the plan.\n *\n * @returns Redirects to the confirmation URL for the payment.\n *\n * @example\n * Using a custom return URL.\n * Change where the merchant is returned to after approving the purchase using the `returnUrl` option.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({\n * plan: MONTHLY_PLAN,\n * isTest: true,\n * returnUrl: '/billing-complete',\n * }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n request: (options: RequestBillingOptions) => Promise;\n\n /**\n * Cancels an ongoing subscription, given its ID.\n *\n * @returns The cancelled subscription.\n *\n * @example\n * Cancelling a subscription.\n * Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.\n * ```ts\n * // /app/routes/cancel-subscription.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * const cancelledSubscription = await billing.cancel({\n * subscriptionId: subscription.id,\n * isTest: true,\n * prorate: true,\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n cancel: (options: CancelBillingOptions) => Promise;\n}" + "value": "export interface EmbeddedAdminContext<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends AdminContextInternal {\n /**\n * The decoded and validated session token for the request.\n *\n * Returned only if `isEmbeddedApp` is `true`.\n *\n * {@link https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload}\n *\n * @example\n * Using the decoded session token.\n * Get user-specific data using the `sessionToken` object.\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * // ...etc\n * useOnlineTokens: true,\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { sessionToken } = await authenticate.public.checkout(\n * request\n * );\n * return json(await getMyAppData({user: sessionToken.sub}));\n * };\n * ```\n */\n sessionToken: JwtPayload;\n\n /**\n * A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded\n * apps.\n *\n * Returned only if `isEmbeddedApp` is `true`.\n *\n * @example\n * Redirecting to an app route.\n * Use the `redirect` helper to safely redirect between pages.\n * ```ts\n * // /app/routes/admin/my-route.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { session, redirect } = await authenticate.admin(request);\n * return redirect(\"/\");\n * };\n * ```\n *\n * @example\n * Redirecting outside of Shopify admin.\n * Pass in a `target` option of `_top` or `_parent` to go to an external URL.\n * ```ts\n * // /app/routes/admin/my-route.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { session, redirect } = await authenticate.admin(request);\n * return redirect(\"/\", { target: '_parent' });\n * };\n * ```\n */\n redirect: RedirectFunction;\n}" }, - "RequireBillingOptions": { - "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "RequireBillingOptions", + "RedirectFunction": { + "filePath": "/server/authenticate/admin/helpers/redirect.ts", + "name": "RedirectFunction", "description": "", - "members": [ + "params": [ { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "plans", - "value": "(keyof Config[\"billing\"])[]", - "description": "The plans to check for. Must be one of the values defined in the `billing` config option." + "name": "url", + "description": "", + "value": "string", + "filePath": "/server/authenticate/admin/helpers/redirect.ts" }, { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "onFailure", - "value": "(error: any) => Promise", - "description": "How to handle the request if the shop doesn't have an active payment for any plan." - }, - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "isTest", - "value": "boolean", + "name": "init", "description": "", - "isOptional": true + "value": "RedirectInit", + "isOptional": true, + "filePath": "/server/authenticate/admin/helpers/redirect.ts" } ], - "value": "export interface RequireBillingOptions\n extends Omit {\n /**\n * The plans to check for. Must be one of the values defined in the `billing` config option.\n */\n plans: (keyof Config['billing'])[];\n /**\n * How to handle the request if the shop doesn't have an active payment for any plan.\n */\n onFailure: (error: any) => Promise;\n}" + "returns": { + "filePath": "/server/authenticate/admin/helpers/redirect.ts", + "description": "", + "name": "TypedResponse", + "value": "TypedResponse" + }, + "value": "export type RedirectFunction = (\n url: string,\n init?: RedirectInit,\n) => TypedResponse;" }, - "RequestBillingOptions": { - "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "RequestBillingOptions", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "plan", - "value": "keyof Config[\"billing\"]", - "description": "The plan to request. Must be one of the values defined in the `billing` config option." - } - ], - "value": "export interface RequestBillingOptions\n extends Omit {\n /**\n * The plan to request. Must be one of the values defined in the `billing` config option.\n */\n plan: keyof Config['billing'];\n}" + "RedirectInit": { + "filePath": "/server/authenticate/admin/helpers/redirect.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "RedirectInit", + "value": "number | (ResponseInit & {target?: RedirectTarget})", + "description": "" }, - "CancelBillingOptions": { - "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "CancelBillingOptions", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "subscriptionId", - "value": "string", - "description": "The ID of the subscription to cancel." - }, - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "prorate", - "value": "boolean", - "description": "Whether to prorate the cancellation.\n\n\n\n\n", - "isOptional": true - }, - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "isTest", - "value": "boolean", - "description": "", - "isOptional": true - } - ], - "value": "export interface CancelBillingOptions {\n /**\n * The ID of the subscription to cancel.\n */\n subscriptionId: string;\n /**\n * Whether to prorate the cancellation.\n *\n * {@link https://shopify.dev/docs/apps/billing/subscriptions/cancel-recurring-charges}\n */\n prorate?: boolean;\n isTest?: boolean;\n}" + "RedirectTarget": { + "filePath": "/server/authenticate/admin/helpers/redirect.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "RedirectTarget", + "value": "'_self' | '_parent' | '_top'", + "description": "" } } } ], "jsDocTypeExamples": [ + "EmbeddedAdminContext", + "AdminApiContext", "BillingContext" ], "related": [ { - "name": "Admin context", - "subtitle": "Authenticate requests from Shopify Admin.", - "url": "/docs/api/shopify-app-remix/authenticate/admin" + "name": "API context", + "subtitle": "Interact with the Admin API.", + "url": "/docs/api/shopify-app-remix/apis/admin-api" + }, + { + "name": "Billing context", + "subtitle": "Bill merchants for your app using the Admin API.", + "url": "/docs/api/shopify-app-remix/apis/billing" } ], "examples": { "description": "", "exampleGroups": [ { - "title": "require", + "title": "sessionToken", "examples": [ { - "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", + "description": "Get user-specific data using the `sessionToken` object.", "codeblock": { - "title": "Requesting billing right away", + "title": "Using the decoded session token", "tabs": [ { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", + "title": "shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", "language": "typescript" }, { - "title": "shopify.server.ts", - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { sessionToken } = await authenticate.public.checkout(\n request\n );\n return json(await getMyAppData({user: sessionToken.sub}));\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "redirect", + "examples": [ + { + "description": "Use the `redirect` helper to safely redirect between pages.", + "codeblock": { + "title": "Redirecting to an app route", + "tabs": [ + { + "title": "/app/routes/admin/my-route.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\");\n};", "language": "typescript" } ] } }, { - "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", + "description": "Pass in a `target` option of `_top` or `_parent` to go to an external URL.", "codeblock": { - "title": "Using a plan selection page", + "title": "Redirecting outside of Shopify admin", "tabs": [ { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", + "title": "/app/routes/admin/my-route.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\", { target: '_parent' });\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "session", + "examples": [ + { + "description": "Get your app's shop-specific data using an offline session.", + "codeblock": { + "title": "Using offline sessions", + "tabs": [ + { + "title": "shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", "language": "typescript" }, + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", + "language": "typescript" + } + ] + } + }, + { + "description": "Get your app's user-specific data using an online session.", + "codeblock": { + "title": "Using online sessions", + "tabs": [ { "title": "shopify.server.ts", - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + }, + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", "language": "typescript" } ] @@ -593,21 +764,57 @@ ] }, { - "title": "request", + "title": "cors", "examples": [ { - "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", + "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", "codeblock": { - "title": "Using a custom return URL", + "title": "Setting CORS headers for a admin request", + "tabs": [ + { + "title": "/app/routes/admin/my-route.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "rest", + "examples": [ + { + "description": "Getting the number of orders in a store using REST resources.", + "codeblock": { + "title": "Using REST resources", "tabs": [ + { + "title": "/app/shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + }, { "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", + "language": "typescript" + } + ] + } + }, + { + "description": "Use `admin.rest.` to make custom requests to the API.", + "codeblock": { + "title": "Performing a GET request to the REST API", + "tabs": [ + { + "title": "/app/shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", "language": "typescript" }, { - "title": "shopify.server.ts", - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", "language": "typescript" } ] @@ -616,16 +823,34 @@ ] }, { - "title": "cancel", + "title": "graphql", "examples": [ { - "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", + "description": "Use `admin.graphql` to make query / mutation requests.", "codeblock": { - "title": "Cancelling a subscription", + "title": "Querying the GraphQL API", "tabs": [ { - "title": "/app/routes/cancel-subscription.ts", - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", + "title": "Example", + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "require", + "examples": [ + { + "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", + "codeblock": { + "title": "Requesting billing right away", + "tabs": [ + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", "language": "typescript" }, { @@ -635,378 +860,190 @@ } ] } - } - ] - } - ] - } - }, - { - "name": "Admin", - "description": "Contains functions for authenticating and interacting with the Admin API.\n\nThis function can handle requests for apps embedded in the Admin, Admin extensions, or non-embedded apps.", - "category": "Authenticate", + }, + { + "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", + "codeblock": { + "title": "Using a plan selection page", + "tabs": [ + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", + "language": "typescript" + }, + { + "title": "shopify.server.ts", + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "request", + "examples": [ + { + "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", + "codeblock": { + "title": "Using a custom return URL", + "tabs": [ + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", + "language": "typescript" + }, + { + "title": "shopify.server.ts", + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "cancel", + "examples": [ + { + "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", + "codeblock": { + "title": "Cancelling a subscription", + "tabs": [ + { + "title": "/app/routes/cancel-subscription.ts", + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", + "language": "typescript" + }, + { + "title": "shopify.server.ts", + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + } + ] + } + } + ] + } + ] + } + }, + { + "name": "Billing", + "description": "Contains function used to bill merchants for your app.\n\nThis object is returned on authenticated Admin requests.", + "category": "APIs", "type": "object", "isVisualComponent": false, "definitions": [ { - "title": "authenticate.admin", - "description": "Authenticates requests coming from the Shopify admin.\n\nThe shape of the returned object changes depending on the `isEmbeddedApp` config.", - "type": "AuthenticateAdmin", + "title": "billing", + "description": "Provides utilities that apps can use to request billing for the app using the Admin API.", + "type": "BillingContext", "typeDefinitions": { - "AuthenticateAdmin": { - "filePath": "/server/types.ts", - "name": "AuthenticateAdmin", - "description": "", - "params": [ - { - "name": "request", - "description": "", - "value": "Request", - "filePath": "/server/types.ts" - } - ], - "returns": { - "filePath": "/server/types.ts", - "description": "", - "name": "Promise>", - "value": "Promise>" - }, - "value": "type AuthenticateAdmin<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> = (request: Request) => Promise>;" - }, - "AdminContext": { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "AdminContext", - "value": "Config['isEmbeddedApp'] extends false\n ? NonEmbeddedAdminContext\n : EmbeddedAdminContext", - "description": "" - }, - "NonEmbeddedAdminContext": { - "filePath": "/server/authenticate/admin/types.ts", - "name": "NonEmbeddedAdminContext", + "BillingContext": { + "filePath": "/server/authenticate/admin/billing/types.ts", + "name": "BillingContext", "description": "", "members": [ { - "filePath": "/server/authenticate/admin/types.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "session", - "value": "Session", - "description": "The session for the user who made the request.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nUse this to get shop or user-specific data.", + "name": "require", + "value": "(options: RequireBillingOptions) => Promise", + "description": "Checks if the shop has an active payment for any plan defined in the `billing` config option.", "examples": [ { - "title": "Using offline sessions", - "description": "Get your app's shop-specific data using an offline session.", + "title": "Requesting billing right away", + "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", "tabs": [ { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", + "title": "/app/routes/**\\/*.ts" }, { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", - "title": "/app/routes/**\\/*.ts" + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" } ] }, { - "title": "Using online sessions", - "description": "Get your app's user-specific data using an online session.", + "title": "Using a plan selection page", + "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", "tabs": [ { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" + "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", + "title": "/app/routes/**\\/*.ts" }, { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", - "title": "/app/routes/**\\/*.ts" + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" } ] } ] }, { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "admin", - "value": "AdminApiContext", - "description": "Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request." - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "billing", - "value": "BillingContext", - "description": "Billing methods for this store, based on the plans defined in the `billing` config option.\n\n\n\n\n" - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "cors", - "value": "EnsureCORSFunction", - "description": "A function that ensures the CORS headers are set correctly for the response.", - "examples": [ - { - "title": "Setting CORS headers for a admin request", - "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", - "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", - "title": "/app/routes/admin/my-route.ts" - } - ] - } - ] - } - ], - "value": "export interface NonEmbeddedAdminContext<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends AdminContextInternal {}" - }, - "AdminApiContext": { - "filePath": "/server/config-types.ts", - "name": "AdminApiContext", - "description": "", - "members": [ - { - "filePath": "/server/config-types.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "rest", - "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", + "name": "request", + "value": "(options: RequestBillingOptions) => Promise", + "description": "Requests payment for the plan.", "examples": [ { - "title": "Using REST resources", - "description": "Getting the number of orders in a store using REST resources.", + "title": "Using a custom return URL", + "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", "tabs": [ { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "/app/shopify.server.ts" - }, - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", "title": "/app/routes/**\\/*.ts" - } - ] - }, - { - "title": "Performing a GET request to the REST API", - "description": "Use `admin.rest.` to make custom requests to the API.", - "tabs": [ + }, { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", - "title": "/app/routes/**\\/*.ts" + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" } ] } ] }, { - "filePath": "/server/config-types.ts", + "filePath": "/server/authenticate/admin/billing/types.ts", "syntaxKind": "PropertySignature", - "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", + "name": "cancel", + "value": "(options: CancelBillingOptions) => Promise", + "description": "Cancels an ongoing subscription, given its ID.", "examples": [ { - "title": "Querying the GraphQL API", - "description": "Use `admin.graphql` to make query / mutation requests.", + "title": "Cancelling a subscription", + "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", "tabs": [ { - "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "title": "Example" + "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", + "title": "/app/routes/cancel-subscription.ts" + }, + { + "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "shopify.server.ts" } ] } ] } ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" - }, - "RestClientWithResources": { - "filePath": "/server/clients/admin/rest.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "RestClientWithResources", - "value": "RemixRestClient & {resources: Resources}", - "description": "" + "value": "export interface BillingContext {\n /**\n * Checks if the shop has an active payment for any plan defined in the `billing` config option.\n *\n * @returns A promise that resolves to an object containing the active purchases for the shop.\n *\n * @example\n * Requesting billing right away.\n * Call `billing.request` in the `onFailure` callback to immediately request payment.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * isTest: true,\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * @example\n * Using a plan selection page.\n * Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, redirect } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n * isTest: true,\n * onFailure: () => redirect('/select-plan'),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n require: (\n options: RequireBillingOptions,\n ) => Promise;\n\n /**\n * Requests payment for the plan.\n *\n * @returns Redirects to the confirmation URL for the payment.\n *\n * @example\n * Using a custom return URL.\n * Change where the merchant is returned to after approving the purchase using the `returnUrl` option.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({\n * plan: MONTHLY_PLAN,\n * isTest: true,\n * returnUrl: '/billing-complete',\n * }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n request: (options: RequestBillingOptions) => Promise;\n\n /**\n * Cancels an ongoing subscription, given its ID.\n *\n * @returns The cancelled subscription.\n *\n * @example\n * Cancelling a subscription.\n * Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.\n * ```ts\n * // /app/routes/cancel-subscription.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * const cancelledSubscription = await billing.cancel({\n * subscriptionId: subscription.id,\n * isTest: true,\n * prorate: true,\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n cancel: (options: CancelBillingOptions) => Promise;\n}" }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", + "RequireBillingOptions": { + "filePath": "/server/authenticate/admin/billing/types.ts", + "name": "RequireBillingOptions", "description": "", - "params": [ + "members": [ { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" - }, - { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" - } - ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" - }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", - "description": "", - "isOptional": true - } - ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" - }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" - } - ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" - }, - "BillingContext": { - "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "BillingContext", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "require", - "value": "(options: RequireBillingOptions) => Promise", - "description": "Checks if the shop has an active payment for any plan defined in the `billing` config option.", - "examples": [ - { - "title": "Requesting billing right away", - "description": "Call `billing.request` in the `onFailure` callback to immediately request payment.", - "tabs": [ - { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n isTest: true,\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" - }, - { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - } - ] - }, - { - "title": "Using a plan selection page", - "description": "Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.", - "tabs": [ - { - "code": "import { LoaderArgs, redirect } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n isTest: true,\n onFailure: () => redirect('/select-plan'),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" - }, - { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "request", - "value": "(options: RequestBillingOptions) => Promise", - "description": "Requests payment for the plan.", - "examples": [ - { - "title": "Using a custom return URL", - "description": "Change where the merchant is returned to after approving the purchase using the `returnUrl` option.", - "tabs": [ - { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({\n plan: MONTHLY_PLAN,\n isTest: true,\n returnUrl: '/billing-complete',\n }),\n });\n\n // App logic\n};", - "title": "/app/routes/**\\/*.ts" - }, - { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "cancel", - "value": "(options: CancelBillingOptions) => Promise", - "description": "Cancels an ongoing subscription, given its ID.", - "examples": [ - { - "title": "Cancelling a subscription", - "description": "Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.", - "tabs": [ - { - "code": "import { LoaderArgs } from \"@remix-run/node\";\nimport { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { billing } = await authenticate.admin(request);\n const billingCheck = await billing.require({\n plans: [MONTHLY_PLAN],\n onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n });\n\n const subscription = billingCheck.appSubscriptions[0];\n const cancelledSubscription = await billing.cancel({\n subscriptionId: subscription.id,\n isTest: true,\n prorate: true,\n });\n\n // App logic\n};", - "title": "/app/routes/cancel-subscription.ts" - }, - { - "code": "import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n\nexport const MONTHLY_PLAN = 'Monthly subscription';\nexport const ANNUAL_PLAN = 'Annual subscription';\n\nconst shopify = shopifyApp({\n // ...etc\n billing: {\n [MONTHLY_PLAN]: {\n amount: 5,\n currencyCode: 'USD',\n interval: BillingInterval.Every30Days,\n },\n [ANNUAL_PLAN]: {\n amount: 50,\n currencyCode: 'USD',\n interval: BillingInterval.Annual,\n },\n }\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - } - ] - } - ] - } - ], - "value": "export interface BillingContext {\n /**\n * Checks if the shop has an active payment for any plan defined in the `billing` config option.\n *\n * @returns A promise that resolves to an object containing the active purchases for the shop.\n *\n * @example\n * Requesting billing right away.\n * Call `billing.request` in the `onFailure` callback to immediately request payment.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * isTest: true,\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * @example\n * Using a plan selection page.\n * Redirect to a different page in the `onFailure` callback, where the merchant can select a billing plan.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, redirect } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN, ANNUAL_PLAN],\n * isTest: true,\n * onFailure: () => redirect('/select-plan'),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n require: (\n options: RequireBillingOptions,\n ) => Promise;\n\n /**\n * Requests payment for the plan.\n *\n * @returns Redirects to the confirmation URL for the payment.\n *\n * @example\n * Using a custom return URL.\n * Change where the merchant is returned to after approving the purchase using the `returnUrl` option.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({\n * plan: MONTHLY_PLAN,\n * isTest: true,\n * returnUrl: '/billing-complete',\n * }),\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n request: (options: RequestBillingOptions) => Promise;\n\n /**\n * Cancels an ongoing subscription, given its ID.\n *\n * @returns The cancelled subscription.\n *\n * @example\n * Cancelling a subscription.\n * Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.\n * ```ts\n * // /app/routes/cancel-subscription.ts\n * import { LoaderArgs } from \"@remix-run/node\";\n * import { authenticate, MONTHLY_PLAN } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { billing } = await authenticate.admin(request);\n * const billingCheck = await billing.require({\n * plans: [MONTHLY_PLAN],\n * onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),\n * });\n *\n * const subscription = billingCheck.appSubscriptions[0];\n * const cancelledSubscription = await billing.cancel({\n * subscriptionId: subscription.id,\n * isTest: true,\n * prorate: true,\n * });\n *\n * // App logic\n * };\n * ```\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp, BillingInterval } from \"@shopify/shopify-app-remix/server\";\n *\n * export const MONTHLY_PLAN = 'Monthly subscription';\n * export const ANNUAL_PLAN = 'Annual subscription';\n *\n * const shopify = shopifyApp({\n * // ...etc\n * billing: {\n * [MONTHLY_PLAN]: {\n * amount: 5,\n * currencyCode: 'USD',\n * interval: BillingInterval.Every30Days,\n * },\n * [ANNUAL_PLAN]: {\n * amount: 50,\n * currencyCode: 'USD',\n * interval: BillingInterval.Annual,\n * },\n * }\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n */\n cancel: (options: CancelBillingOptions) => Promise;\n}" - }, - "RequireBillingOptions": { - "filePath": "/server/authenticate/admin/billing/types.ts", - "name": "RequireBillingOptions", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/admin/billing/types.ts", - "syntaxKind": "PropertySignature", - "name": "plans", - "value": "(keyof Config[\"billing\"])[]", - "description": "The plans to check for. Must be one of the values defined in the `billing` config option." + "filePath": "/server/authenticate/admin/billing/types.ts", + "syntaxKind": "PropertySignature", + "name": "plans", + "value": "(keyof Config[\"billing\"])[]", + "description": "The plans to check for. Must be one of the values defined in the `billing` config option." }, { "filePath": "/server/authenticate/admin/billing/types.ts", @@ -1071,368 +1108,23 @@ } ], "value": "export interface CancelBillingOptions {\n /**\n * The ID of the subscription to cancel.\n */\n subscriptionId: string;\n /**\n * Whether to prorate the cancellation.\n *\n * {@link https://shopify.dev/docs/apps/billing/subscriptions/cancel-recurring-charges}\n */\n prorate?: boolean;\n isTest?: boolean;\n}" - }, - "EmbeddedAdminContext": { - "filePath": "/server/authenticate/admin/types.ts", - "name": "EmbeddedAdminContext", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "sessionToken", - "value": "JwtPayload", - "description": "The decoded and validated session token for the request.\n\nReturned only if `isEmbeddedApp` is `true`.\n\n\n\n\n", - "examples": [ - { - "title": "Using the decoded session token", - "description": "Get user-specific data using the `sessionToken` object.", - "tabs": [ - { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - }, - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { sessionToken } = await authenticate.public.checkout(\n request\n );\n return json(await getMyAppData({user: sessionToken.sub}));\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "redirect", - "value": "RedirectFunction", - "description": "A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded apps.\n\nReturned only if `isEmbeddedApp` is `true`.", - "examples": [ - { - "title": "Redirecting to an app route", - "description": "Use the `redirect` helper to safely redirect between pages.", - "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\");\n};", - "title": "/app/routes/admin/my-route.ts" - } - ] - }, - { - "title": "Redirecting outside of Shopify admin", - "description": "Pass in a `target` option of `_top` or `_parent` to go to an external URL.", - "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\", { target: '_parent' });\n};", - "title": "/app/routes/admin/my-route.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "session", - "value": "Session", - "description": "The session for the user who made the request.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nUse this to get shop or user-specific data.", - "examples": [ - { - "title": "Using offline sessions", - "description": "Get your app's shop-specific data using an offline session.", - "tabs": [ - { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - }, - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - }, - { - "title": "Using online sessions", - "description": "Get your app's user-specific data using an online session.", - "tabs": [ - { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "shopify.server.ts" - }, - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "admin", - "value": "AdminApiContext", - "description": "Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request." - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "billing", - "value": "BillingContext", - "description": "Billing methods for this store, based on the plans defined in the `billing` config option.\n\n\n\n\n" - }, - { - "filePath": "/server/authenticate/admin/types.ts", - "syntaxKind": "PropertySignature", - "name": "cors", - "value": "EnsureCORSFunction", - "description": "A function that ensures the CORS headers are set correctly for the response.", - "examples": [ - { - "title": "Setting CORS headers for a admin request", - "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", - "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", - "title": "/app/routes/admin/my-route.ts" - } - ] - } - ] - } - ], - "value": "export interface EmbeddedAdminContext<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends AdminContextInternal {\n /**\n * The decoded and validated session token for the request.\n *\n * Returned only if `isEmbeddedApp` is `true`.\n *\n * {@link https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload}\n *\n * @example\n * Using the decoded session token.\n * Get user-specific data using the `sessionToken` object.\n * ```ts\n * // shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * // ...etc\n * useOnlineTokens: true,\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { sessionToken } = await authenticate.public.checkout(\n * request\n * );\n * return json(await getMyAppData({user: sessionToken.sub}));\n * };\n * ```\n */\n sessionToken: JwtPayload;\n\n /**\n * A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded\n * apps.\n *\n * Returned only if `isEmbeddedApp` is `true`.\n *\n * @example\n * Redirecting to an app route.\n * Use the `redirect` helper to safely redirect between pages.\n * ```ts\n * // /app/routes/admin/my-route.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { session, redirect } = await authenticate.admin(request);\n * return redirect(\"/\");\n * };\n * ```\n *\n * @example\n * Redirecting outside of Shopify admin.\n * Pass in a `target` option of `_top` or `_parent` to go to an external URL.\n * ```ts\n * // /app/routes/admin/my-route.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { session, redirect } = await authenticate.admin(request);\n * return redirect(\"/\", { target: '_parent' });\n * };\n * ```\n */\n redirect: RedirectFunction;\n}" - }, - "RedirectFunction": { - "filePath": "/server/authenticate/admin/helpers/redirect.ts", - "name": "RedirectFunction", - "description": "", - "params": [ - { - "name": "url", - "description": "", - "value": "string", - "filePath": "/server/authenticate/admin/helpers/redirect.ts" - }, - { - "name": "init", - "description": "", - "value": "RedirectInit", - "isOptional": true, - "filePath": "/server/authenticate/admin/helpers/redirect.ts" - } - ], - "returns": { - "filePath": "/server/authenticate/admin/helpers/redirect.ts", - "description": "", - "name": "TypedResponse", - "value": "TypedResponse" - }, - "value": "export type RedirectFunction = (\n url: string,\n init?: RedirectInit,\n) => TypedResponse;" - }, - "RedirectInit": { - "filePath": "/server/authenticate/admin/helpers/redirect.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "RedirectInit", - "value": "number | (ResponseInit & {target?: RedirectTarget})", - "description": "" - }, - "RedirectTarget": { - "filePath": "/server/authenticate/admin/helpers/redirect.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "RedirectTarget", - "value": "'_self' | '_parent' | '_top'", - "description": "" } } } ], "jsDocTypeExamples": [ - "EmbeddedAdminContext", - "AdminApiContext", "BillingContext" ], "related": [ { - "name": "API context", - "subtitle": "Interact with the Admin API.", - "url": "/docs/api/shopify-app-remix/apis/admin-api" - }, - { - "name": "Billing context", - "subtitle": "Bill merchants for your app using the Admin API.", - "url": "/docs/api/shopify-app-remix/apis/billing" + "name": "Admin context", + "subtitle": "Authenticate requests from Shopify Admin.", + "url": "/docs/api/shopify-app-remix/authenticate/admin" } ], "examples": { - "description": "", - "exampleGroups": [ - { - "title": "sessionToken", - "examples": [ - { - "description": "Get user-specific data using the `sessionToken` object.", - "codeblock": { - "title": "Using the decoded session token", - "tabs": [ - { - "title": "shopify.server.ts", - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "language": "typescript" - }, - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { sessionToken } = await authenticate.public.checkout(\n request\n );\n return json(await getMyAppData({user: sessionToken.sub}));\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "redirect", - "examples": [ - { - "description": "Use the `redirect` helper to safely redirect between pages.", - "codeblock": { - "title": "Redirecting to an app route", - "tabs": [ - { - "title": "/app/routes/admin/my-route.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\");\n};", - "language": "typescript" - } - ] - } - }, - { - "description": "Pass in a `target` option of `_top` or `_parent` to go to an external URL.", - "codeblock": { - "title": "Redirecting outside of Shopify admin", - "tabs": [ - { - "title": "/app/routes/admin/my-route.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, redirect } = await authenticate.admin(request);\n return redirect(\"/\", { target: '_parent' });\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "session", - "examples": [ - { - "description": "Get your app's shop-specific data using an offline session.", - "codeblock": { - "title": "Using offline sessions", - "tabs": [ - { - "title": "shopify.server.ts", - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "language": "typescript" - }, - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({shop: session.shop));\n};", - "language": "typescript" - } - ] - } - }, - { - "description": "Get your app's user-specific data using an online session.", - "codeblock": { - "title": "Using online sessions", - "tabs": [ - { - "title": "shopify.server.ts", - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n\nconst shopify = shopifyApp({\n // ...etc\n useOnlineTokens: true,\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "language": "typescript" - }, - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session } = await authenticate.admin(request);\n return json(await getMyAppData({user: session.onlineAccessInfo!.id}));\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "cors", - "examples": [ - { - "description": "Use the `cors` helper to ensure your app can respond to requests from admin extensions.", - "codeblock": { - "title": "Setting CORS headers for a admin request", - "tabs": [ - { - "title": "/app/routes/admin/my-route.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { session, cors } = await authenticate.admin(request);\n return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "rest", - "examples": [ - { - "description": "Getting the number of orders in a store using REST resources.", - "codeblock": { - "title": "Using REST resources", - "tabs": [ - { - "title": "/app/shopify.server.ts", - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "language": "typescript" - }, - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", - "language": "typescript" - } - ] - } - }, - { - "description": "Use `admin.rest.` to make custom requests to the API.", - "codeblock": { - "title": "Performing a GET request to the REST API", - "tabs": [ - { - "title": "/app/routes/**\\/*.ts", - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", - "language": "typescript" - } - ] - } - } - ] - }, - { - "title": "graphql", - "examples": [ - { - "description": "Use `admin.graphql` to make query / mutation requests.", - "codeblock": { - "title": "Querying the GraphQL API", - "tabs": [ - { - "title": "Example", - "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "language": "typescript" - } - ] - } - } - ] - }, + "description": "", + "exampleGroups": [ { "title": "require", "examples": [ @@ -1575,6 +1267,13 @@ "value": "undefined", "description": "No session is available for the shop that made this request. Therefore no methods for interacting with the GraphQL / REST Admin APIs are available." }, + { + "filePath": "/server/authenticate/public/appProxy/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", + "value": "undefined", + "description": "No session is available for the shop that made this request. Therefore no method for interacting with the Storefront API is available." + }, { "filePath": "/server/authenticate/public/appProxy/types.ts", "syntaxKind": "PropertySignature", @@ -1595,7 +1294,7 @@ ] } ], - "value": "export interface AppProxyContext extends Context {\n /**\n * No session is available for the shop that made this request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n */\n session: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available.\n */\n admin: undefined;\n}" + "value": "export interface AppProxyContext extends Context {\n /**\n * No session is available for the shop that made this request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n */\n session: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available.\n */\n admin: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no method for interacting with the Storefront API is available.\n */\n storefront: undefined;\n}" }, "LiquidResponseFunction": { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -1686,221 +1385,9 @@ { "filePath": "/server/authenticate/public/appProxy/types.ts", "syntaxKind": "PropertySignature", - "name": "liquid", - "value": "LiquidResponseFunction", - "description": "A utility for creating a Liquid Response.", - "examples": [ - { - "title": "Rendering liquid content", - "description": "Use the `liquid` helper to render a `Response` with Liquid content.", - "tabs": [ - { - "code": "import {authenticate} from \"~/shopify.server\"\n\nexport async function loader({ request }) {\n const {liquid} = authenticate.public.appProxy(request);\n\n return liquid(\"Hello {{shop.name}}\")\n}", - "title": "app/routes/**\\/.ts" - } - ] - } - ] - } - ], - "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n}" - }, - "AdminApiContext": { - "filePath": "/server/config-types.ts", - "name": "AdminApiContext", - "description": "", - "members": [ - { - "filePath": "/server/config-types.ts", - "syntaxKind": "PropertySignature", - "name": "rest", - "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", - "examples": [ - { - "title": "Using REST resources", - "description": "Getting the number of orders in a store using REST resources.", - "tabs": [ - { - "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", - "title": "/app/shopify.server.ts" - }, - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - }, - { - "title": "Performing a GET request to the REST API", - "description": "Use `admin.rest.` to make custom requests to the API.", - "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - } - ] - }, - { - "filePath": "/server/config-types.ts", - "syntaxKind": "PropertySignature", - "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", - "examples": [ - { - "title": "Querying the GraphQL API", - "description": "Use `admin.graphql` to make query / mutation requests.", - "tabs": [ - { - "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "title": "Example" - } - ] - } - ] - } - ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" - }, - "RestClientWithResources": { - "filePath": "/server/clients/admin/rest.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "RestClientWithResources", - "value": "RemixRestClient & {resources: Resources}", - "description": "" - }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", - "description": "", - "params": [ - { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" - }, - { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" - } - ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" - }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", - "description": "", - "isOptional": true - } - ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" - }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" - } - ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" - } - } - }, - { - "title": "AppProxyContext", - "description": "Object returned by `authenticate.public.appProxy`.", - "type": "AppProxyContextWithSession", - "typeDefinitions": { - "AppProxyContextWithSession": { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "name": "AppProxyContextWithSession", - "description": "", - "members": [ - { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "syntaxKind": "PropertySignature", - "name": "session", - "value": "Session", - "description": "The session for the shop that made the request.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nUse this to get shop or user-specific data.", - "examples": [ - { - "title": "Using the session object", - "description": "Get your app's data using an offline session for the shop that made the request.", - "tabs": [ - { - "code": "import { json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\nimport { getMyAppModelData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }) => {\n const { session } = await authenticate.public.appProxy(request);\n return json(await getMyAppModelData({shop: session.shop));\n};", - "title": "app/routes/**\\/.ts" - } - ] - } - ] - }, - { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "syntaxKind": "PropertySignature", - "name": "admin", - "value": "AdminApiContext", - "description": "Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.", - "examples": [ - { - "title": "Interacting with the Admin API", - "description": "Use the `admin` object to interact with the REST or GraphQL APIs.", - "tabs": [ - { - "code": "import { json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.public.appProxy(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "title": "app/routes/**\\/.ts" - } - ] - } - ] + "name": "storefront", + "value": "StorefrontContext", + "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request" }, { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -1922,19 +1409,19 @@ ] } ], - "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n}" + "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request\n */\n storefront: StorefrontContext;\n}" }, "AdminApiContext": { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "name": "AdminApiContext", "description": "", "members": [ { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "rest", "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", + "description": "Methods for interacting with the Shopify Admin REST API\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", "examples": [ { "title": "Using REST resources", @@ -1954,165 +1441,73 @@ "title": "Performing a GET request to the REST API", "description": "Use `admin.rest.` to make custom requests to the API.", "tabs": [ - { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", - "title": "/app/routes/**\\/*.ts" - } - ] - } - ] - }, - { - "filePath": "/server/config-types.ts", - "syntaxKind": "PropertySignature", - "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", - "examples": [ - { - "title": "Querying the GraphQL API", - "description": "Use `admin.graphql` to make query / mutation requests.", - "tabs": [ - { - "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", - "title": "Example" - } - ] - } - ] - } - ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" - }, - "RestClientWithResources": { - "filePath": "/server/clients/admin/rest.ts", - "syntaxKind": "TypeAliasDeclaration", - "name": "RestClientWithResources", - "value": "RemixRestClient & {resources: Resources}", - "description": "" - }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", - "description": "", - "params": [ - { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" - }, - { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" - } - ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" - }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", + "title": "/app/routes/**\\/*.ts" + } + ] + } + ] }, { - "filePath": "/server/clients/admin/graphql.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", - "description": "", - "isOptional": true - } - ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" - }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" + "name": "graphql", + "value": "GraphQLClient", + "description": "Methods for interacting with the Shopify Admin GraphQL API\n\n\n\n\n\n\n\n\n\n", + "examples": [ + { + "title": "Querying the GraphQL API", + "description": "Use `admin.graphql` to make query / mutation requests.", + "tabs": [ + { + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "title": "Example" + } + ] + } + ] } ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" + "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the Shopify Admin REST API\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the Shopify Admin GraphQL API\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" }, - "LiquidResponseFunction": { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "name": "LiquidResponseFunction", - "description": "", - "params": [ - { - "name": "body", - "description": "", - "value": "string", - "filePath": "/server/authenticate/public/appProxy/types.ts" - }, - { - "name": "initAndOptions", - "description": "", - "value": "number | (ResponseInit & Options)", - "isOptional": true, - "filePath": "/server/authenticate/public/appProxy/types.ts" - } - ], - "returns": { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "description": "", - "name": "Response", - "value": "Response" - }, - "value": "export type LiquidResponseFunction = (\n body: string,\n initAndOptions?: number | (ResponseInit & Options),\n) => Response;" + "RestClientWithResources": { + "filePath": "/server/clients/admin/rest.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "RestClientWithResources", + "value": "RemixRestClient & {resources: Resources}", + "description": "" }, - "Options": { - "filePath": "/server/authenticate/public/appProxy/types.ts", - "name": "Options", + "StorefrontContext": { + "filePath": "/server/clients/storefront/types.ts", + "name": "StorefrontContext", "description": "", "members": [ { - "filePath": "/server/authenticate/public/appProxy/types.ts", + "filePath": "/server/clients/storefront/types.ts", "syntaxKind": "PropertySignature", - "name": "layout", - "value": "boolean", - "description": "", - "isOptional": true + "name": "graphql", + "value": "GraphQLClient", + "description": "Method for interacting with the Shopify Storefront GraphQL API\n\nIf you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n\n\n\n\n", + "examples": [ + { + "title": "Querying the GraphQL API", + "description": "Use `storefront.graphql` to make query / mutation requests.", + "tabs": [ + { + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "title": "Example" + } + ] + } + ] } ], - "value": "interface Options {\n layout?: boolean;\n}" + "value": "export interface StorefrontContext {\n /**\n * Method for interacting with the Shopify Storefront GraphQL API\n *\n * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n *\n * {@link https://shopify.dev/docs/api/storefront}\n *\n * @example\n * Querying the GraphQL API.\n * Use `storefront.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { storefront } = await authenticate.storefront(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" } } } @@ -2122,9 +1517,14 @@ ], "related": [ { - "name": "API context", + "name": "Admin API context", "subtitle": "Interact with the Admin API.", "url": "/docs/api/shopify-app-remix/apis/admin-api" + }, + { + "name": "Storefront API context", + "subtitle": "Interact with the Storefront API.", + "url": "/docs/api/shopify-app-remix/apis/storefront-api" } ], "examples": { @@ -2351,7 +1751,7 @@ "type": "AuthenticateWebhook", "typeDefinitions": { "AuthenticateWebhook": { - "filePath": "/server/types.ts", + "filePath": "/server/authenticate/webhooks/types.ts", "name": "AuthenticateWebhook", "description": "", "params": [ @@ -2359,16 +1759,16 @@ "name": "request", "description": "", "value": "Request", - "filePath": "/server/types.ts" + "filePath": "/server/authenticate/webhooks/types.ts" } ], "returns": { - "filePath": "/server/types.ts", + "filePath": "/server/authenticate/webhooks/types.ts", "description": "", "name": "Promise<\n WebhookContext | WebhookContextWithSession\n>", "value": "Promise<\n WebhookContext | WebhookContextWithSession\n>" }, - "value": "type AuthenticateWebhook<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n Topics = string | number | symbol,\n> = (\n request: Request,\n) => Promise<\n WebhookContext | WebhookContextWithSession\n>;" + "value": "export type AuthenticateWebhook<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n Topics = string | number | symbol,\n> = (\n request: Request,\n) => Promise<\n WebhookContext | WebhookContextWithSession\n>;" }, "WebhookContext": { "filePath": "/server/authenticate/webhooks/types.ts", @@ -2739,14 +2139,14 @@ { "filePath": "/server/types.ts", "syntaxKind": "MethodSignature", - "name": "__@iterator@424", + "name": "__@iterator@527", "value": "() => IterableIterator", "description": "Iterator" }, { "filePath": "/server/types.ts", "syntaxKind": "MethodSignature", - "name": "__@unscopables@426", + "name": "__@unscopables@529", "value": "() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }", "description": "Returns an object whose properties have the value 'true'\r\nwhen they will be absent when used in a 'with' statement." }, @@ -2818,56 +2218,235 @@ ] }, { - "filePath": "/server/authenticate/webhooks/types.ts", + "filePath": "/server/authenticate/webhooks/types.ts", + "syntaxKind": "PropertySignature", + "name": "topic", + "value": "Topics", + "description": "The topic of the webhook.", + "examples": [ + { + "title": "Webhook topic", + "description": "Get the event topic for the webhook.", + "tabs": [ + { + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { topic } = await authenticate.webhook(request);\n\n switch (topic) {\n case \"APP_UNINSTALLED\":\n // Do something when the app is uninstalled.\n break;\n }\n\n return new Response();\n};", + "title": "Example" + } + ] + } + ] + }, + { + "filePath": "/server/authenticate/webhooks/types.ts", + "syntaxKind": "PropertySignature", + "name": "webhookId", + "value": "string", + "description": "A unique ID for the webhook. Useful to keep track of which events your app has already processed.", + "examples": [ + { + "title": "Webhook ID", + "description": "Get the webhook ID.", + "tabs": [ + { + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { webhookId } = await authenticate.webhook(request);\n return new Response();\n};", + "title": "Example" + } + ] + } + ] + }, + { + "filePath": "/server/authenticate/webhooks/types.ts", + "syntaxKind": "PropertySignature", + "name": "payload", + "value": "JSONValue", + "description": "The payload from the webhook request.", + "examples": [ + { + "title": "Webhook payload", + "description": "Get the request's POST payload.", + "tabs": [ + { + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { payload } = await authenticate.webhook(request);\n return new Response();\n};", + "title": "Example" + } + ] + } + ] + } + ], + "value": "export interface WebhookContextWithSession<\n Topics = string | number | symbol,\n Resources extends ShopifyRestResources = any,\n> extends Context {\n /**\n * A session with an offline token for the shop.\n *\n * Returned only if there is a session for the shop.\n */\n session: Session;\n /**\n * An admin context for the webhook.\n *\n * Returned only if there is a session for the shop.\n */\n admin: {\n /** A REST client. */\n rest: InstanceType & Resources;\n /** A GraphQL client. */\n graphql: InstanceType;\n };\n}" + } + } + } + ], + "jsDocTypeExamples": [ + "WebhookContextWithSession" + ], + "related": [], + "examples": { + "description": "", + "exampleGroups": [ + { + "title": "apiVersion", + "examples": [ + { + "description": "Get the API version used for webhook request.", + "codeblock": { + "title": "Webhook API version", + "tabs": [ + { + "title": "Example", + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { apiVersion } = await authenticate.webhook(request);\n return new Response();\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "shop", + "examples": [ + { + "description": "Get the shop that triggered a webhook.", + "codeblock": { + "title": "Webhook shop", + "tabs": [ + { + "title": "Example", + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { shop } = await authenticate.webhook(request);\n return new Response();\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "topic", + "examples": [ + { + "description": "Get the event topic for the webhook.", + "codeblock": { + "title": "Webhook topic", + "tabs": [ + { + "title": "Example", + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { topic } = await authenticate.webhook(request);\n\n switch (topic) {\n case \"APP_UNINSTALLED\":\n // Do something when the app is uninstalled.\n break;\n }\n\n return new Response();\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "webhookId", + "examples": [ + { + "description": "Get the webhook ID.", + "codeblock": { + "title": "Webhook ID", + "tabs": [ + { + "title": "Example", + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { webhookId } = await authenticate.webhook(request);\n return new Response();\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "payload", + "examples": [ + { + "description": "Get the request's POST payload.", + "codeblock": { + "title": "Webhook payload", + "tabs": [ + { + "title": "Example", + "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { payload } = await authenticate.webhook(request);\n return new Response();\n};", + "language": "typescript" + } + ] + } + } + ] + } + ] + } + }, + { + "name": "Admin API", + "description": "Contains objects used to interact with the Admin API.\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).", + "category": "APIs", + "type": "object", + "isVisualComponent": false, + "definitions": [ + { + "title": "admin", + "description": "Provides utilities that apps can use to make requests to the Admin API.", + "type": "AdminApiContext", + "typeDefinitions": { + "AdminApiContext": { + "filePath": "/server/clients/admin/types.ts", + "name": "AdminApiContext", + "description": "", + "members": [ + { + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", - "name": "topic", - "value": "Topics", - "description": "The topic of the webhook.", + "name": "rest", + "value": "RestClientWithResources", + "description": "Methods for interacting with the Shopify Admin REST API\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", "examples": [ { - "title": "Webhook topic", - "description": "Get the event topic for the webhook.", + "title": "Using REST resources", + "description": "Getting the number of orders in a store using REST resources.", "tabs": [ { - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { topic } = await authenticate.webhook(request);\n\n switch (topic) {\n case \"APP_UNINSTALLED\":\n // Do something when the app is uninstalled.\n break;\n }\n\n return new Response();\n};", - "title": "Example" + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", + "title": "/app/routes/**\\/*.ts" } ] - } - ] - }, - { - "filePath": "/server/authenticate/webhooks/types.ts", - "syntaxKind": "PropertySignature", - "name": "webhookId", - "value": "string", - "description": "A unique ID for the webhook. Useful to keep track of which events your app has already processed.", - "examples": [ + }, { - "title": "Webhook ID", - "description": "Get the webhook ID.", + "title": "Performing a GET request to the REST API", + "description": "Use `admin.rest.` to make custom requests to the API.", "tabs": [ { - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { webhookId } = await authenticate.webhook(request);\n return new Response();\n};", - "title": "Example" + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", + "title": "/app/routes/**\\/*.ts" } ] } ] }, { - "filePath": "/server/authenticate/webhooks/types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", - "name": "payload", - "value": "JSONValue", - "description": "The payload from the webhook request.", + "name": "graphql", + "value": "GraphQLClient", + "description": "Methods for interacting with the Shopify Admin GraphQL API\n\n\n\n\n\n\n\n\n\n", "examples": [ { - "title": "Webhook payload", - "description": "Get the request's POST payload.", + "title": "Querying the GraphQL API", + "description": "Use `admin.graphql` to make query / mutation requests.", "tabs": [ { - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { payload } = await authenticate.webhook(request);\n return new Response();\n};", + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", "title": "Example" } ] @@ -2875,47 +2454,70 @@ ] } ], - "value": "export interface WebhookContextWithSession<\n Topics = string | number | symbol,\n Resources extends ShopifyRestResources = any,\n> extends Context {\n /**\n * A session with an offline token for the shop.\n *\n * Returned only if there is a session for the shop.\n */\n session: Session;\n /**\n * An admin context for the webhook.\n *\n * Returned only if there is a session for the shop.\n */\n admin: {\n /** A REST client. */\n rest: InstanceType & Resources;\n /** A GraphQL client. */\n graphql: InstanceType;\n };\n}" + "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the Shopify Admin REST API\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the Shopify Admin GraphQL API\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" + }, + "RestClientWithResources": { + "filePath": "/server/clients/admin/rest.ts", + "syntaxKind": "TypeAliasDeclaration", + "name": "RestClientWithResources", + "value": "RemixRestClient & {resources: Resources}", + "description": "" } } } ], "jsDocTypeExamples": [ - "WebhookContextWithSession" + "AdminApiContext" + ], + "related": [ + { + "name": "Authenticated context", + "subtitle": "Authenticate requests from Shopify Admin.", + "url": "/docs/api/shopify-app-remix/authenticate/admin" + }, + { + "name": "Unauthenticated context", + "subtitle": "Interact with the Admin API on non-Shopify requests.", + "url": "/docs/api/shopify-app-remix/unauthenticated/unauthenticated-admin" + } ], - "related": [], "examples": { "description": "", "exampleGroups": [ { - "title": "apiVersion", + "title": "rest", "examples": [ { - "description": "Get the API version used for webhook request.", + "description": "Getting the number of orders in a store using REST resources.", "codeblock": { - "title": "Webhook API version", + "title": "Using REST resources", "tabs": [ { - "title": "Example", - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { apiVersion } = await authenticate.webhook(request);\n return new Response();\n};", + "title": "/app/shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + }, + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n return json(admin.rest.resources.Order.count({ session }));\n};", "language": "typescript" } ] } - } - ] - }, - { - "title": "shop", - "examples": [ + }, { - "description": "Get the shop that triggered a webhook.", + "description": "Use `admin.rest.` to make custom requests to the API.", "codeblock": { - "title": "Webhook shop", + "title": "Performing a GET request to the REST API", "tabs": [ { - "title": "Example", - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { shop } = await authenticate.webhook(request);\n return new Response();\n};", + "title": "/app/shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + }, + { + "title": "/app/routes/**\\/*.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", "language": "typescript" } ] @@ -2924,52 +2526,97 @@ ] }, { - "title": "topic", + "title": "graphql", "examples": [ { - "description": "Get the event topic for the webhook.", + "description": "Use `admin.graphql` to make query / mutation requests.", "codeblock": { - "title": "Webhook topic", + "title": "Querying the GraphQL API", "tabs": [ { "title": "Example", - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { topic } = await authenticate.webhook(request);\n\n switch (topic) {\n case \"APP_UNINSTALLED\":\n // Do something when the app is uninstalled.\n break;\n }\n\n return new Response();\n};", + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.admin(request);\n\n const response = await admin.graphql(\n `#graphql\n mutation populateProduct($input: ProductInput!) {\n productCreate(input: $input) {\n product {\n id\n }\n }\n }`,\n { variables: { input: { title: \"Product Name\" } } }\n );\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", "language": "typescript" } ] } } ] - }, - { - "title": "webhookId", - "examples": [ - { - "description": "Get the webhook ID.", - "codeblock": { - "title": "Webhook ID", - "tabs": [ + } + ] + } + }, + { + "name": "Storefront API", + "description": "Contains objects used to interact with the Storefront API.\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).", + "category": "APIs", + "type": "object", + "isVisualComponent": false, + "definitions": [ + { + "title": "storefront", + "description": "Provides utilities that apps can use to make requests to the Storefront API.", + "type": "StorefrontContext", + "typeDefinitions": { + "StorefrontContext": { + "filePath": "/server/clients/storefront/types.ts", + "name": "StorefrontContext", + "description": "", + "members": [ + { + "filePath": "/server/clients/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "graphql", + "value": "GraphQLClient", + "description": "Method for interacting with the Shopify Storefront GraphQL API\n\nIf you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n\n\n\n\n", + "examples": [ { - "title": "Example", - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { webhookId } = await authenticate.webhook(request);\n return new Response();\n};", - "language": "typescript" + "title": "Querying the GraphQL API", + "description": "Use `storefront.graphql` to make query / mutation requests.", + "tabs": [ + { + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "title": "Example" + } + ] } ] } - } - ] - }, + ], + "value": "export interface StorefrontContext {\n /**\n * Method for interacting with the Shopify Storefront GraphQL API\n *\n * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n *\n * {@link https://shopify.dev/docs/api/storefront}\n *\n * @example\n * Querying the GraphQL API.\n * Use `storefront.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { storefront } = await authenticate.storefront(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" + } + } + } + ], + "jsDocTypeExamples": [ + "StorefrontContext" + ], + "related": [ + { + "name": "App proxy context", + "subtitle": "Authenticate requests from Shopify app proxies.", + "url": "/docs/api/shopify-app-remix/authenticate/public/app-proxy" + }, + { + "name": "Unauthenticated context", + "subtitle": "Interact with the Storefront API on non-Shopify requests.", + "url": "/docs/api/shopify-app-remix/unauthenticated/unauthenticated-storefront" + } + ], + "examples": { + "description": "", + "exampleGroups": [ { - "title": "payload", + "title": "graphql", "examples": [ { - "description": "Get the request's POST payload.", + "description": "Use `storefront.graphql` to make query / mutation requests.", "codeblock": { - "title": "Webhook payload", + "title": "Querying the GraphQL API", "tabs": [ { "title": "Example", - "code": "import { ActionFunction } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const action: ActionFunction = async ({ request }) => {\n const { payload } = await authenticate.webhook(request);\n return new Response();\n};", + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", "language": "typescript" } ] @@ -3010,7 +2657,7 @@ "name": "ShopifyApp>", "value": "ShopifyApp>" }, - "value": "export function shopifyApp<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources,\n Storage extends SessionStorage,\n>(appConfig: Config): ShopifyApp {\n const api = deriveApi(appConfig);\n const config = deriveConfig(appConfig, api.config);\n const logger = overrideLogger(api.logger);\n\n if (appConfig.webhooks) {\n api.webhooks.addHandlers(appConfig.webhooks);\n }\n\n const params: BasicParams = {api, config, logger};\n const oauth = new AuthStrategy(params);\n\n const shopify:\n | AdminApp\n | AppStoreApp\n | SingleMerchantApp = {\n sessionStorage: config.sessionStorage,\n addDocumentResponseHeaders: addDocumentResponseHeadersFactory(params),\n registerWebhooks: registerWebhooksFactory(params),\n authenticate: {\n admin: oauth.authenticateAdmin.bind(oauth),\n public: authenticatePublicFactory(params),\n webhook: authenticateWebhookFactory<\n Resources,\n keyof Config['webhooks'] | MandatoryTopics\n >(params),\n },\n unauthenticated: {\n admin: unauthenticatedAdminContextFactory(params),\n },\n };\n\n if (\n isAppStoreApp(shopify, appConfig) ||\n isSingleMerchantApp(shopify, appConfig)\n ) {\n shopify.login = loginFactory(params);\n }\n\n return shopify as ShopifyApp;\n}", + "value": "export function shopifyApp<\n Config extends AppConfigArg,\n Resources extends ShopifyRestResources,\n Storage extends SessionStorage,\n>(appConfig: Config): ShopifyApp {\n const api = deriveApi(appConfig);\n const config = deriveConfig(appConfig, api.config);\n const logger = overrideLogger(api.logger);\n\n if (appConfig.webhooks) {\n api.webhooks.addHandlers(appConfig.webhooks);\n }\n\n const params: BasicParams = {api, config, logger};\n const oauth = new AuthStrategy(params);\n\n const shopify:\n | AdminApp\n | AppStoreApp\n | SingleMerchantApp = {\n sessionStorage: config.sessionStorage,\n addDocumentResponseHeaders: addDocumentResponseHeadersFactory(params),\n registerWebhooks: registerWebhooksFactory(params),\n authenticate: {\n admin: oauth.authenticateAdmin.bind(oauth),\n public: authenticatePublicFactory(params),\n webhook: authenticateWebhookFactory<\n Resources,\n keyof Config['webhooks'] | MandatoryTopics\n >(params),\n },\n unauthenticated: {\n admin: unauthenticatedAdminContextFactory(params),\n storefront: unauthenticatedStorefrontContextFactory(params),\n },\n };\n\n if (\n isAppStoreApp(shopify, appConfig) ||\n isSingleMerchantApp(shopify, appConfig)\n ) {\n shopify.login = loginFactory(params);\n }\n\n return shopify as ShopifyApp;\n}", "examples": [ { "title": "The minimum viable configuration", @@ -3309,16 +2956,16 @@ "value": "export interface AfterAuthOptions<\n R extends ShopifyRestResources = ShopifyRestResources,\n> {\n session: Session;\n admin: AdminApiContext;\n}" }, "AdminApiContext": { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "name": "AdminApiContext", "description": "", "members": [ { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "rest", "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", + "description": "Methods for interacting with the Shopify Admin REST API\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", "examples": [ { "title": "Using REST resources", @@ -3338,6 +2985,10 @@ "title": "Performing a GET request to the REST API", "description": "Use `admin.rest.` to make custom requests to the API.", "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, { "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", "title": "/app/routes/**\\/*.ts" @@ -3347,11 +2998,11 @@ ] }, { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", + "value": "GraphQLClient", + "description": "Methods for interacting with the Shopify Admin GraphQL API\n\n\n\n\n\n\n\n\n\n", "examples": [ { "title": "Querying the GraphQL API", @@ -3366,7 +3017,7 @@ ] } ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" + "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the Shopify Admin REST API\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the Shopify Admin GraphQL API\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" }, "RestClientWithResources": { "filePath": "/server/clients/admin/rest.ts", @@ -3375,86 +3026,6 @@ "value": "RemixRestClient & {resources: Resources}", "description": "" }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", - "description": "", - "params": [ - { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" - }, - { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" - } - ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" - }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", - "description": "", - "isOptional": true - } - ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" - }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" - } - ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" - }, "AppDistribution": { "filePath": "/server/types.ts", "syntaxKind": "EnumDeclaration", @@ -3580,7 +3151,7 @@ "filePath": "/server/types.ts", "syntaxKind": "PropertySignature", "name": "unauthenticated", - "value": "Unauthenticated", + "value": "Unauthenticated>", "description": "Ways to get Contexts from requests that do not originate from Shopify.", "examples": [ { @@ -4369,9 +3940,16 @@ { "filePath": "/server/authenticate/public/appProxy/types.ts", "syntaxKind": "PropertySignature", - "name": "admin", + "name": "admin", + "value": "undefined", + "description": "No session is available for the shop that made this request. Therefore no methods for interacting with the GraphQL / REST Admin APIs are available." + }, + { + "filePath": "/server/authenticate/public/appProxy/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", "value": "undefined", - "description": "No session is available for the shop that made this request. Therefore no methods for interacting with the GraphQL / REST Admin APIs are available." + "description": "No session is available for the shop that made this request. Therefore no method for interacting with the Storefront API is available." }, { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -4393,7 +3971,7 @@ ] } ], - "value": "export interface AppProxyContext extends Context {\n /**\n * No session is available for the shop that made this request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n */\n session: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available.\n */\n admin: undefined;\n}" + "value": "export interface AppProxyContext extends Context {\n /**\n * No session is available for the shop that made this request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n */\n session: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no methods for interacting with the GraphQL / REST Admin APIs are available.\n */\n admin: undefined;\n\n /**\n * No session is available for the shop that made this request.\n * Therefore no method for interacting with the Storefront API is available.\n */\n storefront: undefined;\n}" }, "LiquidResponseFunction": { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -4481,6 +4059,13 @@ } ] }, + { + "filePath": "/server/authenticate/public/appProxy/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", + "value": "StorefrontContext", + "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request" + }, { "filePath": "/server/authenticate/public/appProxy/types.ts", "syntaxKind": "PropertySignature", @@ -4501,10 +4086,37 @@ ] } ], - "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n}" + "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request\n */\n storefront: StorefrontContext;\n}" + }, + "StorefrontContext": { + "filePath": "/server/clients/storefront/types.ts", + "name": "StorefrontContext", + "description": "", + "members": [ + { + "filePath": "/server/clients/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "graphql", + "value": "GraphQLClient", + "description": "Method for interacting with the Shopify Storefront GraphQL API\n\nIf you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n\n\n\n\n", + "examples": [ + { + "title": "Querying the GraphQL API", + "description": "Use `storefront.graphql` to make query / mutation requests.", + "tabs": [ + { + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "title": "Example" + } + ] + } + ] + } + ], + "value": "export interface StorefrontContext {\n /**\n * Method for interacting with the Shopify Storefront GraphQL API\n *\n * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n *\n * {@link https://shopify.dev/docs/api/storefront}\n *\n * @example\n * Querying the GraphQL API.\n * Use `storefront.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { storefront } = await authenticate.storefront(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" }, "AuthenticateWebhook": { - "filePath": "/server/types.ts", + "filePath": "/server/authenticate/webhooks/types.ts", "name": "AuthenticateWebhook", "description": "", "params": [ @@ -4512,16 +4124,16 @@ "name": "request", "description": "", "value": "Request", - "filePath": "/server/types.ts" + "filePath": "/server/authenticate/webhooks/types.ts" } ], "returns": { - "filePath": "/server/types.ts", + "filePath": "/server/authenticate/webhooks/types.ts", "description": "", "name": "Promise<\n WebhookContext | WebhookContextWithSession\n>", "value": "Promise<\n WebhookContext | WebhookContextWithSession\n>" }, - "value": "type AuthenticateWebhook<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n Topics = string | number | symbol,\n> = (\n request: Request,\n) => Promise<\n WebhookContext | WebhookContextWithSession\n>;" + "value": "export type AuthenticateWebhook<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n Topics = string | number | symbol,\n> = (\n request: Request,\n) => Promise<\n WebhookContext | WebhookContextWithSession\n>;" }, "WebhookContext": { "filePath": "/server/authenticate/webhooks/types.ts", @@ -4892,14 +4504,14 @@ { "filePath": "/server/types.ts", "syntaxKind": "MethodSignature", - "name": "__@iterator@424", + "name": "__@iterator@527", "value": "() => IterableIterator", "description": "Iterator" }, { "filePath": "/server/types.ts", "syntaxKind": "MethodSignature", - "name": "__@unscopables@426", + "name": "__@unscopables@529", "value": "() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }", "description": "Returns an object whose properties have the value 'true'\r\nwhen they will be absent when used in a 'with' statement." }, @@ -5038,19 +4650,19 @@ "description": "" }, "Unauthenticated": { - "filePath": "/server/types.ts", + "filePath": "/server/unauthenticated/types.ts", "name": "Unauthenticated", "description": "", "members": [ { - "filePath": "/server/types.ts", + "filePath": "/server/unauthenticated/types.ts", "syntaxKind": "PropertySignature", "name": "admin", - "value": "UnauthenticatedAdmin>", + "value": "GetUnauthenticatedAdminContext", "description": "Get an admin context by passing a shop\n\n**Warning** This should only be used for Requests that do not originate from Shopify. You must do your own authentication before using this method. This method throws an error if there is no session for the shop.", "examples": [ { - "title": "Responding to a request from an external service not controlled by Shopify", + "title": "Responding to a request not controlled by Shopify", "description": "", "tabs": [ { @@ -5064,29 +4676,48 @@ ] } ] + }, + { + "filePath": "/server/unauthenticated/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", + "value": "GetUnauthenticatedStorefrontContext", + "description": "Get a storefront context by passing a shop\n\n**Warning** This should only be used for Requests that do not originate from Shopify. You must do your own authentication before using this method. This method throws an error if there is no session for the shop.", + "examples": [ + { + "title": "Responding to a request not controlled by Shopify", + "description": "", + "tabs": [ + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticateExternal } from \"~/helpers/authenticate\"\nimport shopify from \"../../shopify.server\";\n\nexport async function loader({ request }: LoaderArgs) {\n const shop = await authenticateExternal(request)\n const {storefront} = await shopify.unauthenticated.storefront(shop);\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`)\n\n return json(await response.json());\n}", + "title": "/app/routes/**\\/*.jsx" + } + ] + } + ] } ], - "value": "interface Unauthenticated {\n /**\n * Get an admin context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request from an external service not controlled by Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n admin: UnauthenticatedAdmin>;\n}" + "value": "export interface Unauthenticated {\n /**\n * Get an admin context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n admin: GetUnauthenticatedAdminContext;\n\n /**\n * Get a storefront context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {storefront} = await shopify.unauthenticated.storefront(shop);\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`)\n *\n * return json(await response.json());\n * }\n * ```\n */\n storefront: GetUnauthenticatedStorefrontContext;\n}" }, - "UnauthenticatedAdmin": { - "filePath": "/server/types.ts", - "name": "UnauthenticatedAdmin", + "GetUnauthenticatedAdminContext": { + "filePath": "/server/unauthenticated/admin/types.ts", + "name": "GetUnauthenticatedAdminContext", "description": "", "params": [ { "name": "shop", "description": "", "value": "string", - "filePath": "/server/types.ts" + "filePath": "/server/unauthenticated/admin/types.ts" } ], "returns": { - "filePath": "/server/types.ts", + "filePath": "/server/unauthenticated/admin/types.ts", "description": "", "name": "Promise>", "value": "Promise>" }, - "value": "type UnauthenticatedAdmin = (\n shop: string,\n) => Promise>;" + "value": "export type GetUnauthenticatedAdminContext<\n Resources extends ShopifyRestResources,\n> = (shop: string) => Promise>;" }, "UnauthenticatedAdminContext": { "filePath": "/server/unauthenticated/admin/types.ts", @@ -5122,6 +4753,60 @@ ], "value": "export interface UnauthenticatedAdminContext<\n Resources extends ShopifyRestResources,\n> {\n /**\n * The session for the given shop.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * This will always be an offline session. You can use to get shop-specific data.\n *\n * @example\n * Using the offline session.\n * Get your app's shop-specific data using the returned offline `session` object.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { unauthenticated } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const shop = getShopFromExternalRequest(request);\n * const { session } = await unauthenticated.admin(shop);\n * return json(await getMyAppData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the given store.\n */\n admin: AdminApiContext;\n}" }, + "GetUnauthenticatedStorefrontContext": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "name": "GetUnauthenticatedStorefrontContext", + "description": "", + "params": [ + { + "name": "shop", + "description": "", + "value": "string", + "filePath": "/server/unauthenticated/storefront/types.ts" + } + ], + "returns": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "description": "", + "name": "Promise", + "value": "Promise" + }, + "value": "export type GetUnauthenticatedStorefrontContext = (\n shop: string,\n) => Promise;" + }, + "UnauthenticatedStorefrontContext": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "name": "UnauthenticatedStorefrontContext", + "description": "", + "members": [ + { + "filePath": "/server/unauthenticated/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "session", + "value": "Session", + "description": "The session for the given shop.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nThis will always be an offline session. You can use this to get shop specific data.", + "examples": [ + { + "title": "Using the offline session", + "description": "Get your app's shop-specific data using the returned offline `session` object.", + "tabs": [ + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { unauthenticated } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const shop = getShopFromExternalRequest(request);\n const { session } = await unauthenticated.storefront(shop);\n return json(await getMyAppData({shop: session.shop));\n};", + "title": "app/routes/**\\/.ts" + } + ] + } + ] + }, + { + "filePath": "/server/unauthenticated/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", + "value": "StorefrontContext", + "description": "Method for interacting with the Shopify GraphQL Storefront API for the given store." + } + ], + "value": "export interface UnauthenticatedStorefrontContext {\n /**\n * The session for the given shop.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * This will always be an offline session. You can use this to get shop specific data.\n *\n * @example\n * Using the offline session.\n * Get your app's shop-specific data using the returned offline `session` object.\n * ```ts\n * // app/routes/**\\/.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { unauthenticated } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const shop = getShopFromExternalRequest(request);\n * const { session } = await unauthenticated.storefront(shop);\n * return json(await getMyAppData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Method for interacting with the Shopify GraphQL Storefront API for the given store.\n */\n storefront: StorefrontContext;\n}" + }, "SingleMerchantApp": { "filePath": "/server/types.ts", "syntaxKind": "TypeAliasDeclaration", @@ -5222,7 +4907,7 @@ "filePath": "/server/types.ts", "syntaxKind": "PropertySignature", "name": "unauthenticated", - "value": "Unauthenticated", + "value": "Unauthenticated>", "description": "Ways to get Contexts from requests that do not originate from Shopify.", "examples": [ { @@ -5242,7 +4927,7 @@ ] } ], - "value": "export interface ShopifyAppBase {\n /**\n * The `SessionStorage` instance you passed in as a config option.\n *\n * @example\n * Storing sessions with Prisma.\n * Import the `@shopify/shopify-app-session-storage-prisma` package to store sessions in your Prisma database.\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { PrismaSessionStorage } from \"@shopify/shopify-app-session-storage-prisma\";\n * import prisma from \"~/db.server\";\n *\n * const shopify = shopifyApp({\n * sesssionStorage: new PrismaSessionStorage(prisma),\n * // ...etc\n * })\n *\n * // shopify.sessionStorage is an instance of PrismaSessionStorage\n * ```\n */\n sessionStorage: SessionStorageType;\n\n /**\n * Adds the required Content Security Policy headers for Shopify apps to the given Headers object.\n *\n * {@link https://shopify.dev/docs/apps/store/security/iframe-protection}\n *\n * @example\n * Return headers on all requests.\n * Add headers to all HTML requests by calling `shopify.addDocumentResponseHeaders` in `entry.server.tsx`.\n *\n * ```\n * // ~/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * // ...etc\n * });\n * export default shopify;\n * export const addDocumentResponseheaders = shopify.addDocumentResponseheaders;\n * ```\n *\n * ```ts\n * // entry.server.tsx\n * import { addDocumentResponseHeaders } from \"~/shopify.server\";\n *\n * export default function handleRequest(\n * request: Request,\n * responseStatusCode: number,\n * responseHeaders: Headers,\n * remixContext: EntryContext\n * ) {\n * const markup = renderToString(\n * \n * );\n *\n * responseHeaders.set(\"Content-Type\", \"text/html\");\n * addDocumentResponseHeaders(request, responseHeaders);\n *\n * return new Response(\"\" + markup, {\n * status: responseStatusCode,\n * headers: responseHeaders,\n * });\n * }\n * ```\n */\n addDocumentResponseHeaders: AddDocumentResponseHeaders;\n\n /**\n * Register webhook topics for a store using the given session. Most likely you want to use this in combination with the afterAuth hook.\n *\n * @example\n * Registering webhooks.\n * Trigger the registration after a merchant installs your app using the `afterAuth` hook.\n * ```ts\n * import { DeliveryMethod, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * hooks: {\n * afterAuth: async ({ session }) => {\n * shopify.registerWebhooks({ session });\n * }\n * },\n * webhooks: {\n * APP_UNINSTALLED: {\n * deliveryMethod: DeliveryMethod.Http,\n * callbackUrl: \"/webhooks\",\n * },\n * },\n * // ...etc\n * });\n * ```\n */\n registerWebhooks: RegisterWebhooks;\n\n /**\n * Ways to authenticate requests from different surfaces across Shopify.\n *\n * @example\n * Authenticate Shopify requests.\n * Use the functions in `authenticate` to validate requests coming from Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const {admin, session, sessionToken, billing} = shopify.authenticate.admin(request);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n authenticate: Authenticate;\n\n /**\n * Ways to get Contexts from requests that do not originate from Shopify.\n *\n * @example\n * Using unauthenticated contexts.\n * Create contexts for requests that don't come from Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n unauthenticated: Unauthenticated;\n}" + "value": "export interface ShopifyAppBase {\n /**\n * The `SessionStorage` instance you passed in as a config option.\n *\n * @example\n * Storing sessions with Prisma.\n * Import the `@shopify/shopify-app-session-storage-prisma` package to store sessions in your Prisma database.\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { PrismaSessionStorage } from \"@shopify/shopify-app-session-storage-prisma\";\n * import prisma from \"~/db.server\";\n *\n * const shopify = shopifyApp({\n * sesssionStorage: new PrismaSessionStorage(prisma),\n * // ...etc\n * })\n *\n * // shopify.sessionStorage is an instance of PrismaSessionStorage\n * ```\n */\n sessionStorage: SessionStorageType;\n\n /**\n * Adds the required Content Security Policy headers for Shopify apps to the given Headers object.\n *\n * {@link https://shopify.dev/docs/apps/store/security/iframe-protection}\n *\n * @example\n * Return headers on all requests.\n * Add headers to all HTML requests by calling `shopify.addDocumentResponseHeaders` in `entry.server.tsx`.\n *\n * ```\n * // ~/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * // ...etc\n * });\n * export default shopify;\n * export const addDocumentResponseheaders = shopify.addDocumentResponseheaders;\n * ```\n *\n * ```ts\n * // entry.server.tsx\n * import { addDocumentResponseHeaders } from \"~/shopify.server\";\n *\n * export default function handleRequest(\n * request: Request,\n * responseStatusCode: number,\n * responseHeaders: Headers,\n * remixContext: EntryContext\n * ) {\n * const markup = renderToString(\n * \n * );\n *\n * responseHeaders.set(\"Content-Type\", \"text/html\");\n * addDocumentResponseHeaders(request, responseHeaders);\n *\n * return new Response(\"\" + markup, {\n * status: responseStatusCode,\n * headers: responseHeaders,\n * });\n * }\n * ```\n */\n addDocumentResponseHeaders: AddDocumentResponseHeaders;\n\n /**\n * Register webhook topics for a store using the given session. Most likely you want to use this in combination with the afterAuth hook.\n *\n * @example\n * Registering webhooks.\n * Trigger the registration after a merchant installs your app using the `afterAuth` hook.\n * ```ts\n * import { DeliveryMethod, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n *\n * const shopify = shopifyApp({\n * hooks: {\n * afterAuth: async ({ session }) => {\n * shopify.registerWebhooks({ session });\n * }\n * },\n * webhooks: {\n * APP_UNINSTALLED: {\n * deliveryMethod: DeliveryMethod.Http,\n * callbackUrl: \"/webhooks\",\n * },\n * },\n * // ...etc\n * });\n * ```\n */\n registerWebhooks: RegisterWebhooks;\n\n /**\n * Ways to authenticate requests from different surfaces across Shopify.\n *\n * @example\n * Authenticate Shopify requests.\n * Use the functions in `authenticate` to validate requests coming from Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const {admin, session, sessionToken, billing} = shopify.authenticate.admin(request);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n authenticate: Authenticate;\n\n /**\n * Ways to get Contexts from requests that do not originate from Shopify.\n *\n * @example\n * Using unauthenticated contexts.\n * Create contexts for requests that don't come from Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n unauthenticated: Unauthenticated>;\n}" }, "ShopifyAppLogin": { "filePath": "/server/types.ts", @@ -5513,27 +5198,27 @@ { "title": "unauthenticated.admin", "description": "Creates an unauthenticated Admin context.", - "type": "UnauthenticatedAdmin", + "type": "GetUnauthenticatedAdminContext", "typeDefinitions": { - "UnauthenticatedAdmin": { - "filePath": "/server/types.ts", - "name": "UnauthenticatedAdmin", + "GetUnauthenticatedAdminContext": { + "filePath": "/server/unauthenticated/admin/types.ts", + "name": "GetUnauthenticatedAdminContext", "description": "", "params": [ { "name": "shop", "description": "", "value": "string", - "filePath": "/server/types.ts" + "filePath": "/server/unauthenticated/admin/types.ts" } ], "returns": { - "filePath": "/server/types.ts", + "filePath": "/server/unauthenticated/admin/types.ts", "description": "", "name": "Promise>", "value": "Promise>" }, - "value": "type UnauthenticatedAdmin = (\n shop: string,\n) => Promise>;" + "value": "export type GetUnauthenticatedAdminContext<\n Resources extends ShopifyRestResources,\n> = (shop: string) => Promise>;" }, "UnauthenticatedAdminContext": { "filePath": "/server/unauthenticated/admin/types.ts", @@ -5570,16 +5255,16 @@ "value": "export interface UnauthenticatedAdminContext<\n Resources extends ShopifyRestResources,\n> {\n /**\n * The session for the given shop.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * This will always be an offline session. You can use to get shop-specific data.\n *\n * @example\n * Using the offline session.\n * Get your app's shop-specific data using the returned offline `session` object.\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { unauthenticated } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const shop = getShopFromExternalRequest(request);\n * const { session } = await unauthenticated.admin(shop);\n * return json(await getMyAppData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the given store.\n */\n admin: AdminApiContext;\n}" }, "AdminApiContext": { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "name": "AdminApiContext", "description": "", "members": [ { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "rest", "value": "RestClientWithResources", - "description": "Methods for interacting with the REST Admin API.\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", + "description": "Methods for interacting with the Shopify Admin REST API\n\nThere are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n\n\n\n\n", "examples": [ { "title": "Using REST resources", @@ -5599,6 +5284,10 @@ "title": "Performing a GET request to the REST API", "description": "Use `admin.rest.` to make custom requests to the API.", "tabs": [ + { + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "title": "/app/shopify.server.ts" + }, { "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", "title": "/app/routes/**\\/*.ts" @@ -5608,11 +5297,11 @@ ] }, { - "filePath": "/server/config-types.ts", + "filePath": "/server/clients/admin/types.ts", "syntaxKind": "PropertySignature", "name": "graphql", - "value": "GraphqlQueryFunction", - "description": "Methods for interacting with the GraphQL Admin API.\n\n\n\n\n\n\n\n\n\n", + "value": "GraphQLClient", + "description": "Methods for interacting with the Shopify Admin GraphQL API\n\n\n\n\n\n\n\n\n\n", "examples": [ { "title": "Querying the GraphQL API", @@ -5627,7 +5316,7 @@ ] } ], - "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the REST Admin API.\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the GraphQL Admin API.\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphqlQueryFunction;\n}" + "value": "export interface AdminApiContext<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> {\n /**\n * Methods for interacting with the Shopify Admin REST API\n *\n * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs.\n *\n * {@link https://shopify.dev/docs/api/admin-rest}\n *\n * @example\n * Using REST resources.\n * Getting the number of orders in a store using REST resources.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-07\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * return json(admin.rest.resources.Order.count({ session }));\n * };\n * ```\n *\n * @example\n * Performing a GET request to the REST API.\n * Use `admin.rest.` to make custom requests to the API.\n *\n * ```ts\n * // /app/shopify.server.ts\n * import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * export const authenticate = shopify.authenticate;\n * ```\n *\n * ```ts\n * // /app/routes/**\\/*.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const { admin, session } = await authenticate.admin(request);\n * const response = await admin.rest.get({ path: \"/customers/count.json\" });\n * const customers = await response.json();\n * return json({ customers });\n * };\n * ```\n */\n rest: RestClientWithResources;\n\n /**\n * Methods for interacting with the Shopify Admin GraphQL API\n *\n * {@link https://shopify.dev/docs/api/admin-graphql}\n * {@link https://github.com/Shopify/shopify-api-js/blob/main/docs/reference/clients/Graphql.md}\n *\n * @example\n * Querying the GraphQL API.\n * Use `admin.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.admin(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" }, "RestClientWithResources": { "filePath": "/server/clients/admin/rest.ts", @@ -5635,86 +5324,6 @@ "name": "RestClientWithResources", "value": "RemixRestClient & {resources: Resources}", "description": "" - }, - "GraphqlQueryFunction": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "GraphqlQueryFunction", - "description": "", - "params": [ - { - "name": "query", - "description": "", - "value": "string", - "filePath": "/server/clients/admin/graphql.ts" - }, - { - "name": "options", - "description": "", - "value": "QueryOptions", - "isOptional": true, - "filePath": "/server/clients/admin/graphql.ts" - } - ], - "returns": { - "filePath": "/server/clients/admin/graphql.ts", - "description": "", - "name": "Promise", - "value": "Promise" - }, - "value": "export type GraphqlQueryFunction = (\n query: string,\n options?: QueryOptions,\n) => Promise;" - }, - "QueryOptions": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryOptions", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "variables", - "value": "QueryVariables", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "apiVersion", - "value": "ApiVersion", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "headers", - "value": "{ [key: string]: any; }", - "description": "", - "isOptional": true - }, - { - "filePath": "/server/clients/admin/graphql.ts", - "syntaxKind": "PropertySignature", - "name": "tries", - "value": "number", - "description": "", - "isOptional": true - } - ], - "value": "interface QueryOptions {\n variables?: QueryVariables;\n apiVersion?: ApiVersion;\n headers?: {[key: string]: any};\n tries?: number;\n}" - }, - "QueryVariables": { - "filePath": "/server/clients/admin/graphql.ts", - "name": "QueryVariables", - "description": "", - "members": [ - { - "filePath": "/server/clients/admin/graphql.ts", - "name": "[key: string]", - "value": "any" - } - ], - "value": "interface QueryVariables {\n [key: string]: any;\n}" } } } @@ -5777,6 +5386,11 @@ "codeblock": { "title": "Performing a GET request to the REST API", "tabs": [ + { + "title": "/app/shopify.server.ts", + "code": "import { shopifyApp } from \"@shopify/shopify-app-remix/server\";\nimport { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n\nconst shopify = shopifyApp({\n restResources,\n // ...etc\n});\nexport default shopify;\nexport const authenticate = shopify.authenticate;", + "language": "typescript" + }, { "title": "/app/routes/**\\/*.ts", "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const { admin, session } = await authenticate.admin(request);\n const response = await admin.rest.get({ path: \"/customers/count.json\" });\n const customers = await response.json();\n return json({ customers });\n};", @@ -5807,5 +5421,154 @@ } ] } + }, + { + "name": "Unauthenticated storefront", + "description": "Allows interacting with the Storefront API on requests that didn't come from Shopify.\n\n> Caution: This should only be used for Requests that do not originate from Shopify.\n> You must do your own authentication before using this method.\n>This function doesn't perform **any** validation and shouldn't rely on unvalidated user input.", + "category": "Unauthenticated", + "type": "object", + "isVisualComponent": false, + "definitions": [ + { + "title": "unauthenticated.storefront", + "description": "Creates an unauthenticated Storefront context.", + "type": "GetUnauthenticatedStorefrontContext", + "typeDefinitions": { + "GetUnauthenticatedStorefrontContext": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "name": "GetUnauthenticatedStorefrontContext", + "description": "", + "params": [ + { + "name": "shop", + "description": "", + "value": "string", + "filePath": "/server/unauthenticated/storefront/types.ts" + } + ], + "returns": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "description": "", + "name": "Promise", + "value": "Promise" + }, + "value": "export type GetUnauthenticatedStorefrontContext = (\n shop: string,\n) => Promise;" + }, + "UnauthenticatedStorefrontContext": { + "filePath": "/server/unauthenticated/storefront/types.ts", + "name": "UnauthenticatedStorefrontContext", + "description": "", + "members": [ + { + "filePath": "/server/unauthenticated/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "session", + "value": "Session", + "description": "The session for the given shop.\n\nThis comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n\nThis will always be an offline session. You can use this to get shop specific data.", + "examples": [ + { + "title": "Using the offline session", + "description": "Get your app's shop-specific data using the returned offline `session` object.", + "tabs": [ + { + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { unauthenticated } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const shop = getShopFromExternalRequest(request);\n const { session } = await unauthenticated.storefront(shop);\n return json(await getMyAppData({shop: session.shop));\n};", + "title": "app/routes/**\\/.ts" + } + ] + } + ] + }, + { + "filePath": "/server/unauthenticated/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "storefront", + "value": "StorefrontContext", + "description": "Method for interacting with the Shopify GraphQL Storefront API for the given store." + } + ], + "value": "export interface UnauthenticatedStorefrontContext {\n /**\n * The session for the given shop.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * This will always be an offline session. You can use this to get shop specific data.\n *\n * @example\n * Using the offline session.\n * Get your app's shop-specific data using the returned offline `session` object.\n * ```ts\n * // app/routes/**\\/.ts\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { unauthenticated } from \"../shopify.server\";\n * import { getMyAppData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }: LoaderArgs) => {\n * const shop = getShopFromExternalRequest(request);\n * const { session } = await unauthenticated.storefront(shop);\n * return json(await getMyAppData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Method for interacting with the Shopify GraphQL Storefront API for the given store.\n */\n storefront: StorefrontContext;\n}" + }, + "StorefrontContext": { + "filePath": "/server/clients/storefront/types.ts", + "name": "StorefrontContext", + "description": "", + "members": [ + { + "filePath": "/server/clients/storefront/types.ts", + "syntaxKind": "PropertySignature", + "name": "graphql", + "value": "GraphQLClient", + "description": "Method for interacting with the Shopify Storefront GraphQL API\n\nIf you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n\n\n\n\n", + "examples": [ + { + "title": "Querying the GraphQL API", + "description": "Use `storefront.graphql` to make query / mutation requests.", + "tabs": [ + { + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "title": "Example" + } + ] + } + ] + } + ], + "value": "export interface StorefrontContext {\n /**\n * Method for interacting with the Shopify Storefront GraphQL API\n *\n * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints).\n *\n * {@link https://shopify.dev/docs/api/storefront}\n *\n * @example\n * Querying the GraphQL API.\n * Use `storefront.graphql` to make query / mutation requests.\n * ```ts\n * import { ActionArgs } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { storefront } = await authenticate.storefront(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n graphql: GraphQLClient;\n}" + } + } + } + ], + "jsDocTypeExamples": [ + "UnauthenticatedStorefrontContext", + "StorefrontContext" + ], + "related": [ + { + "name": "API context", + "subtitle": "Interact with the Storefront API.", + "url": "/docs/api/shopify-app-remix/apis/storefront-api" + } + ], + "examples": { + "description": "", + "exampleGroups": [ + { + "title": "session", + "examples": [ + { + "description": "Get your app's shop-specific data using the returned offline `session` object.", + "codeblock": { + "title": "Using the offline session", + "tabs": [ + { + "title": "app/routes/**\\/.ts", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { unauthenticated } from \"../shopify.server\";\nimport { getMyAppData } from \"~/db/model.server\";\n\nexport const loader = async ({ request }: LoaderArgs) => {\n const shop = getShopFromExternalRequest(request);\n const { session } = await unauthenticated.storefront(shop);\n return json(await getMyAppData({shop: session.shop));\n};", + "language": "typescript" + } + ] + } + } + ] + }, + { + "title": "graphql", + "examples": [ + { + "description": "Use `storefront.graphql` to make query / mutation requests.", + "codeblock": { + "title": "Querying the GraphQL API", + "tabs": [ + { + "title": "Example", + "code": "import { ActionArgs } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { storefront } = await authenticate.storefront(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n const productData = await response.json();\n return json({ data: productData.data });\n}", + "language": "typescript" + } + ] + } + } + ] + } + ] + } } ] \ No newline at end of file diff --git a/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.admin.billing.doc.ts b/packages/shopify-app-remix/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts similarity index 100% rename from packages/shopify-app-remix/src/server/authenticate/admin/authenticate.admin.billing.doc.ts rename to packages/shopify-app-remix/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts index d9459815c2..3219007c71 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts @@ -14,19 +14,19 @@ const data: ReferenceEntityTemplateSchema = { description: 'Authenticates requests coming from Shopify app proxies.', type: 'AuthenticateAppProxy', }, - { - title: 'AppProxyContext', - description: 'Object returned by `authenticate.public.appProxy`.', - type: 'AppProxyContextWithSession', - }, ], jsDocTypeExamples: ['AppProxyContextWithSession'], related: [ { - name: 'API context', + name: 'Admin API context', subtitle: 'Interact with the Admin API.', url: '/docs/api/shopify-app-remix/apis/admin-api', }, + { + name: 'Storefront API context', + subtitle: 'Interact with the Storefront API.', + url: '/docs/api/shopify-app-remix/apis/storefront-api', + }, ], }; diff --git a/packages/shopify-app-remix/src/server/authenticate/admin/authenticate.admin.api.doc.ts b/packages/shopify-app-remix/src/server/clients/admin/authenticate.admin.api.doc.ts similarity index 100% rename from packages/shopify-app-remix/src/server/authenticate/admin/authenticate.admin.api.doc.ts rename to packages/shopify-app-remix/src/server/clients/admin/authenticate.admin.api.doc.ts diff --git a/packages/shopify-app-remix/src/server/clients/storefront/authenticate.storefront.api.doc.ts b/packages/shopify-app-remix/src/server/clients/storefront/authenticate.storefront.api.doc.ts new file mode 100644 index 0000000000..f86766074f --- /dev/null +++ b/packages/shopify-app-remix/src/server/clients/storefront/authenticate.storefront.api.doc.ts @@ -0,0 +1,34 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'Storefront API', + description: + 'Contains objects used to interact with the Storefront API.' + + '\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).', + category: 'APIs', + type: 'object', + isVisualComponent: false, + definitions: [ + { + title: 'storefront', + description: + 'Provides utilities that apps can use to make requests to the Storefront API.', + type: 'StorefrontContext', + }, + ], + jsDocTypeExamples: ['StorefrontContext'], + related: [ + { + name: 'App proxy context', + subtitle: 'Authenticate requests from Shopify app proxies.', + url: '/docs/api/shopify-app-remix/authenticate/public/app-proxy', + }, + { + name: 'Unauthenticated context', + subtitle: 'Interact with the Storefront API on non-Shopify requests.', + url: '/docs/api/shopify-app-remix/unauthenticated/unauthenticated-storefront', + }, + ], +}; + +export default data; diff --git a/packages/shopify-app-remix/src/server/clients/storefront/types.ts b/packages/shopify-app-remix/src/server/clients/storefront/types.ts index 724792ad59..396b066c82 100644 --- a/packages/shopify-app-remix/src/server/clients/storefront/types.ts +++ b/packages/shopify-app-remix/src/server/clients/storefront/types.ts @@ -4,28 +4,21 @@ export interface StorefrontContext { /** * Method for interacting with the Shopify Storefront GraphQL API * + * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints). + * * {@link https://shopify.dev/docs/api/storefront} * * @example - * Getting a list of blog posts a new product + * Querying the GraphQL API. + * Use `storefront.graphql` to make query / mutation requests. * ```ts * import { ActionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export async function action({ request }: ActionArgs) { - * const { admin } = await authenticate.admin(request); + * const { storefront } = await authenticate.storefront(request); * - * const response = await admin.graphql( - * `#graphql - * mutation populateProduct($input: ProductInput!) { - * productCreate(input: $input) { - * product { - * id - * } - * } - * }`, - * { variables: { input: { title: "Product Name" } } } - * ); + * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`); * * const productData = await response.json(); * return json({ data: productData.data }); diff --git a/packages/shopify-app-remix/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts b/packages/shopify-app-remix/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts index 5ca8d6405a..73be2efb97 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts @@ -14,7 +14,7 @@ const data: ReferenceEntityTemplateSchema = { { title: 'unauthenticated.admin', description: 'Creates an unauthenticated Admin context.', - type: 'UnauthenticatedAdmin', + type: 'GetUnauthenticatedAdminContext', }, ], jsDocTypeExamples: ['UnauthenticatedAdminContext', 'AdminApiContext'], diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts index 50add0786f..04b9e4d0a3 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/types.ts @@ -11,17 +11,18 @@ export interface UnauthenticatedStorefrontContext { * This will always be an offline session. You can use this to get shop specific data. * * @example - * Getting shop specific data using a session + * Using the offline session. + * Get your app's shop-specific data using the returned offline `session` object. * ```ts * // app/routes/**\/.ts * import { LoaderArgs, json } from "@remix-run/node"; * import { unauthenticated } from "../shopify.server"; - * import { getWidgets } from "~/db/widgets.server"; + * import { getMyAppData } from "~/db/model.server"; * * export const loader = async ({ request }: LoaderArgs) => { * const shop = getShopFromExternalRequest(request); * const { session } = await unauthenticated.storefront(shop); - * return json(await getWidgets({shop: session.shop)); + * return json(await getMyAppData({shop: session.shop)); * }; * ``` */ diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts new file mode 100644 index 0000000000..2456cec4ad --- /dev/null +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts @@ -0,0 +1,30 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'Unauthenticated storefront', + description: + "Allows interacting with the Storefront API on requests that didn't come from Shopify." + + '\n\n> Caution: This should only be used for Requests that do not originate from Shopify.' + + '\n> You must do your own authentication before using this method.' + + "\n>This function doesn't perform **any** validation and shouldn't rely on unvalidated user input.", + category: 'Unauthenticated', + type: 'object', + isVisualComponent: false, + definitions: [ + { + title: 'unauthenticated.storefront', + description: 'Creates an unauthenticated Storefront context.', + type: 'GetUnauthenticatedStorefrontContext', + }, + ], + jsDocTypeExamples: ['UnauthenticatedStorefrontContext', 'StorefrontContext'], + related: [ + { + name: 'API context', + subtitle: 'Interact with the Storefront API.', + url: '/docs/api/shopify-app-remix/apis/storefront-api', + }, + ], +}; + +export default data; From 62624424ebd47219032207bba2fbf0fe945b2caf Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:03:22 -0400 Subject: [PATCH 12/14] Applying comments from PR --- .../docs/generated/generated_docs_data.json | 54 ++++++++++++++++--- .../authenticate/public/appProxy/types.ts | 19 ++++++- .../src/server/unauthenticated/types.ts | 2 +- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/shopify-app-remix/docs/generated/generated_docs_data.json b/packages/shopify-app-remix/docs/generated/generated_docs_data.json index 0b24b2d1c7..02be7eec34 100644 --- a/packages/shopify-app-remix/docs/generated/generated_docs_data.json +++ b/packages/shopify-app-remix/docs/generated/generated_docs_data.json @@ -1387,7 +1387,19 @@ "syntaxKind": "PropertySignature", "name": "storefront", "value": "StorefrontContext", - "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request" + "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request.", + "examples": [ + { + "title": "Interacting with the Storefront API", + "description": "Use the `storefront` object to interact with the GraphQL API.", + "tabs": [ + { + "code": "import { json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.public.appProxy(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n return json(await response.json());\n}", + "title": "app/routes/**\\/.ts" + } + ] + } + ] }, { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -1409,7 +1421,7 @@ ] } ], - "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request\n */\n storefront: StorefrontContext;\n}" + "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request.\n *\n * @example\n * Interacting with the Storefront API.\n * Use the `storefront` object to interact with the GraphQL API.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * return json(await response.json());\n * }\n * ```\n */\n storefront: StorefrontContext;\n}" }, "AdminApiContext": { "filePath": "/server/clients/admin/types.ts", @@ -1566,6 +1578,24 @@ } ] }, + { + "title": "storefront", + "examples": [ + { + "description": "Use the `storefront` object to interact with the GraphQL API.", + "codeblock": { + "title": "Interacting with the Storefront API", + "tabs": [ + { + "title": "app/routes/**\\/.ts", + "code": "import { json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.public.appProxy(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n return json(await response.json());\n}", + "language": "typescript" + } + ] + } + } + ] + }, { "title": "liquid", "examples": [ @@ -4064,7 +4094,19 @@ "syntaxKind": "PropertySignature", "name": "storefront", "value": "StorefrontContext", - "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request" + "description": "Method for interacting with the Shopify Storefront Graphql API for the store that made the request.", + "examples": [ + { + "title": "Interacting with the Storefront API", + "description": "Use the `storefront` object to interact with the GraphQL API.", + "tabs": [ + { + "code": "import { json } from \"@remix-run/node\";\nimport { authenticate } from \"../shopify.server\";\n\nexport async function action({ request }: ActionArgs) {\n const { admin } = await authenticate.public.appProxy(request);\n\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n return json(await response.json());\n}", + "title": "app/routes/**\\/.ts" + } + ] + } + ] }, { "filePath": "/server/authenticate/public/appProxy/types.ts", @@ -4086,7 +4128,7 @@ ] } ], - "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request\n */\n storefront: StorefrontContext;\n}" + "value": "export interface AppProxyContextWithSession<\n Resources extends ShopifyRestResources = ShopifyRestResources,\n> extends Context {\n /**\n * The session for the shop that made the request.\n *\n * This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice.\n *\n * Use this to get shop or user-specific data.\n *\n * @example\n * Using the session object.\n * Get your app's data using an offline session for the shop that made the request.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n * import { getMyAppModelData } from \"~/db/model.server\";\n *\n * export const loader = async ({ request }) => {\n * const { session } = await authenticate.public.appProxy(request);\n * return json(await getMyAppModelData({shop: session.shop));\n * };\n * ```\n */\n session: Session;\n\n /**\n * Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.\n *\n * @example\n * Interacting with the Admin API.\n * Use the `admin` object to interact with the REST or GraphQL APIs.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await admin.graphql(\n * `#graphql\n * mutation populateProduct($input: ProductInput!) {\n * productCreate(input: $input) {\n * product {\n * id\n * }\n * }\n * }`,\n * { variables: { input: { title: \"Product Name\" } } }\n * );\n *\n * const productData = await response.json();\n * return json({ data: productData.data });\n * }\n * ```\n */\n admin: AdminApiContext;\n\n /**\n * Method for interacting with the Shopify Storefront Graphql API for the store that made the request.\n *\n * @example\n * Interacting with the Storefront API.\n * Use the `storefront` object to interact with the GraphQL API.\n * ```ts\n * // app/routes/**\\/.ts\n * import { json } from \"@remix-run/node\";\n * import { authenticate } from \"../shopify.server\";\n *\n * export async function action({ request }: ActionArgs) {\n * const { admin } = await authenticate.public.appProxy(request);\n *\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * return json(await response.json());\n * }\n * ```\n */\n storefront: StorefrontContext;\n}" }, "StorefrontContext": { "filePath": "/server/clients/storefront/types.ts", @@ -4689,7 +4731,7 @@ "description": "", "tabs": [ { - "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticateExternal } from \"~/helpers/authenticate\"\nimport shopify from \"../../shopify.server\";\n\nexport async function loader({ request }: LoaderArgs) {\n const shop = await authenticateExternal(request)\n const {storefront} = await shopify.unauthenticated.storefront(shop);\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`)\n\n return json(await response.json());\n}", + "code": "import { LoaderArgs, json } from \"@remix-run/node\";\nimport { authenticateExternal } from \"~/helpers/authenticate\"\nimport shopify from \"../../shopify.server\";\n\nexport async function loader({ request }: LoaderArgs) {\n const shop = await authenticateExternal(request)\n const {storefront} = await shopify.unauthenticated.storefront(shop);\n const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n\n return json(await response.json());\n}", "title": "/app/routes/**\\/*.jsx" } ] @@ -4697,7 +4739,7 @@ ] } ], - "value": "export interface Unauthenticated {\n /**\n * Get an admin context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n admin: GetUnauthenticatedAdminContext;\n\n /**\n * Get a storefront context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {storefront} = await shopify.unauthenticated.storefront(shop);\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`)\n *\n * return json(await response.json());\n * }\n * ```\n */\n storefront: GetUnauthenticatedStorefrontContext;\n}" + "value": "export interface Unauthenticated {\n /**\n * Get an admin context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify.\n * ```ts\n * // /app/shopify.server.ts\n * import { LATEST_API_VERSION, shopifyApp } from \"@shopify/shopify-app-remix/server\";\n * import { restResources } from \"@shopify/shopify-api/rest/admin/2023-04\";\n *\n * const shopify = shopifyApp({\n * restResources,\n * // ...etc\n * });\n * export default shopify;\n * ```\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {admin} = await shopify.unauthenticated.admin(shop);\n *\n * return json(await admin.rest.resources.Product.count({ session }));\n * }\n * ```\n */\n admin: GetUnauthenticatedAdminContext;\n\n /**\n * Get a storefront context by passing a shop\n *\n * **Warning** This should only be used for Requests that do not originate from Shopify.\n * You must do your own authentication before using this method.\n * This method throws an error if there is no session for the shop.\n *\n * @example\n * Responding to a request not controlled by Shopify\n * ```ts\n * // /app/routes/**\\/*.jsx\n * import { LoaderArgs, json } from \"@remix-run/node\";\n * import { authenticateExternal } from \"~/helpers/authenticate\"\n * import shopify from \"../../shopify.server\";\n *\n * export async function loader({ request }: LoaderArgs) {\n * const shop = await authenticateExternal(request)\n * const {storefront} = await shopify.unauthenticated.storefront(shop);\n * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`);\n *\n * return json(await response.json());\n * }\n * ```\n */\n storefront: GetUnauthenticatedStorefrontContext;\n}" }, "GetUnauthenticatedAdminContext": { "filePath": "/server/unauthenticated/admin/types.ts", diff --git a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts index f3c2863f03..44d52122b6 100644 --- a/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts +++ b/packages/shopify-app-remix/src/server/authenticate/public/appProxy/types.ts @@ -119,7 +119,24 @@ export interface AppProxyContextWithSession< admin: AdminApiContext; /** - * Method for interacting with the Shopify Storefront Graphql API for the store that made the request + * Method for interacting with the Shopify Storefront Graphql API for the store that made the request. + * + * @example + * Interacting with the Storefront API. + * Use the `storefront` object to interact with the GraphQL API. + * ```ts + * // app/routes/**\/.ts + * import { json } from "@remix-run/node"; + * import { authenticate } from "../shopify.server"; + * + * export async function action({ request }: ActionArgs) { + * const { admin } = await authenticate.public.appProxy(request); + * + * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`); + * + * return json(await response.json()); + * } + * ``` */ storefront: StorefrontContext; } diff --git a/packages/shopify-app-remix/src/server/unauthenticated/types.ts b/packages/shopify-app-remix/src/server/unauthenticated/types.ts index d8b3dbbfc3..4b80d9e84a 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/types.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/types.ts @@ -58,7 +58,7 @@ export interface Unauthenticated { * export async function loader({ request }: LoaderArgs) { * const shop = await authenticateExternal(request) * const {storefront} = await shopify.unauthenticated.storefront(shop); - * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`) + * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`); * * return json(await response.json()); * } From 644433c79ee85efbc32fcc3c34b964b9f289b389 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 22 Sep 2023 11:39:12 -0400 Subject: [PATCH 13/14] PR Comments: 1. Update Error message. It's a storefront context. not an admin context --- .../src/server/unauthenticated/storefront/factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts index fda1c3a9cf..e592cecaed 100644 --- a/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts +++ b/packages/shopify-app-remix/src/server/unauthenticated/storefront/factory.ts @@ -17,7 +17,7 @@ export function unauthenticatedStorefrontContextFactory( if (!session) { throw new ShopifyError( - `Could not find a session for shop ${shop} when creating unauthenticated admin context`, + `Could not find a session for shop ${shop} when creating unauthenticated storefront context`, ); } From 6ac683261a709fc724485bd25f44353fa694f8d1 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 22 Sep 2023 12:00:12 -0400 Subject: [PATCH 14/14] Added Changelog --- .changeset/thirty-comics-attack.md | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .changeset/thirty-comics-attack.md diff --git a/.changeset/thirty-comics-attack.md b/.changeset/thirty-comics-attack.md new file mode 100644 index 0000000000..f267b436c8 --- /dev/null +++ b/.changeset/thirty-comics-attack.md @@ -0,0 +1,46 @@ +--- +'@shopify/shopify-app-remix': minor +--- + +Added the storefront GraphQL client. + +The storefront API client can be accessed in two ways + +
+ App Proxy + +```ts +import {json} from '@remix-run/node'; +import {authenticate} from '~/shopify.server'; + +export async function loader({request}) { + const {storefront} = await authenticate.public.appProxy(request); + const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}'); + + return json(await response.json()); +} +``` + +
+ +
+ Unauthenticated Storefront + +```ts +import {json} from '@remix-run/node'; +import {unauthenticated} from '~/shopify.server'; +import {customAuthenticateRequest} from '~/helpers'; + +export async function loader({request}) { + await customAuthenticateRequest(request); + + const {storefront} = await unauthenticated.storefront( + 'my-shop.myshopify.com', + ); + const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}'); + + return json(await response.json()); +} +``` + +