Contains functions for authenticating and interacting with the Admin API. This function can handle requests for apps embedded in the Admin, Admin extensions, or non-embedded apps.
import {type ActionFunctionArgs, json} from '@remix-run/node';
import {GraphqlQueryError} from '@shopify/shopify-api';
import {authenticate} from '../shopify.server';
export const action = async ({request}: ActionFunctionArgs) => {
const {admin, redirect} = await authenticate.admin(request);
try {
await admin.graphql(
`#graphql
mutation updateProductTitle($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
}
}
}`,
{
variables: {
input: {id: '123', title: 'New title'},
},
},
);
return redirect('/app/product-updated');
} catch (error) {
if (error instanceof GraphqlQueryError) {
return json({errors: error.body?.errors}, {status: 500});
}
return new Response('Failed to update product title', {status: 500});
}
};
Authenticates requests coming from the Shopify admin. The shape of the returned object changes depending on the `isEmbeddedApp` config.
request: Request
export type AuthenticateAdmin<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> = (request: Request) => Promise
FeatureEnabled
Future extends FutureFlags ? Future[Flag] extends true ? true : false : false
When enabled, embedded apps will fetch access tokens via [token exchange](https://shopify.dev/docs/apps/auth/get-access-tokens/token-exchange). This assumes the app has scopes declared for [Shopify managing installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation). Learn more about this [new embedded app auth strategy](https://shopify.dev/docs/api/shopify-app-remix#embedded-auth-strategy).
When enabled, the Scopes API will be available. This feature is in development and requires special permissions from Shopify for now.
Config['isEmbeddedApp'] extends false
? NonEmbeddedAdminContext
Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.
Billing methods for this store, based on the plans defined in the `billing` config option.
A function that ensures the CORS headers are set correctly for the response.
The session for the user who made the request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. Use this to get shop or user-specific data.
Cancels an ongoing subscription, given its ID.
Checks if the shop has an active payment for any plan defined in the `billing` config option.
Creates a usage record for an app subscription.
Requests payment for the plan.
Checks if the shop has an active payment for any plan defined in the `billing` config option.
Updates the capped amount for a usage billing plan.
Whether to use the test mode. This prevents the credit card from being charged.
Whether to issue prorated credits for the unused portion of the app subscription. There will be a corresponding deduction (based on revenue share) to your Partner account. For example, if a $10.00 app subscription (with 0% revenue share) is cancelled and prorated half way through the billing cycle, then the merchant will be credited $5.00 and that amount will be deducted from your Partner account.
The ID of the subscription to cancel.
Whether to include charges that were created on test mode. Test shops and demo shops cannot be charged.
The plans to check for. Must be one of the values defined in the `billing` config option.
The description of the app usage record.
Whether to use the test mode. This prevents the credit card from being charged.
The price of the app usage record.
Whether to use the test mode. This prevents the credit card from being charged. Test shops and demo shops cannot be charged.
The plan to request. Must be one of the values defined in the `billing` config option.
The URL to return to after the merchant approves the payment.
Whether to include charges that were created on test mode. Test shops and demo shops cannot be charged.
How to handle the request if the shop doesn't have an active payment for any plan.
The plans to check for. Must be one of the values defined in the `billing` config option.
The maximum charge for the usage billing plan.
The subscription line item ID to update.
export interface EnsureCORSFunction { (response: Response): Response; }
Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.
Billing methods for this store, based on the plans defined in the `billing` config option.
A function that ensures the CORS headers are set correctly for the response.
A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded apps. Returned only if `isEmbeddedApp` is `true`.
The session for the user who made the request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. Use this to get shop or user-specific data.
The decoded and validated session token for the request. Returned only if `isEmbeddedApp` is `true`.
url: string
init: RedirectInit
export type RedirectFunction = (
url: string,
init?: RedirectInit,
) => TypedResponse
number | (ResponseInit & {target?: RedirectTarget})
'_self' | '_parent' | '_top' | '_blank'
Methods to manage optional scopes for the store that made the request.
Contains functions for authenticating and interacting with the Admin API. This function can handle requests for apps embedded in the Admin, Admin extensions, or non-embedded apps.
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { getMyAppData } from "~/db/model.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session, cors } = await authenticate.admin(request);
return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id})));
};
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session, redirect } = await authenticate.admin(request);
return redirect("/");
};
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session, redirect } = await authenticate.admin(request);
return redirect("/", { target: '_parent' });
};
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { getMyAppData } from "~/db/model.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
return json(await getMyAppData({shop: session.shop));
};
import { shopifyApp } from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
// ...etc
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { getMyAppData } from "~/db/model.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
return json(await getMyAppData({user: session.onlineAccessInfo!.id}));
};
import { shopifyApp } from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
// ...etc
useOnlineTokens: true,
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { getMyAppData } from "~/db/model.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { sessionToken } = await authenticate.admin(
request
);
return json(await getMyAppData({user: sessionToken.sub}));
};
import { shopifyApp } from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
// ...etc
useOnlineTokens: true,
});
export default shopify;
export const authenticate = shopify.authenticate;
import { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
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({
productId: productData.data?.productCreate?.product?.id,
});
}
import { shopifyApp } from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
// ...
});
export default shopify;
export const authenticate = shopify.authenticate;
import { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { admin } = await authenticate.admin(request);
try {
const response = await admin.graphql(
`#graphql
query incorrectQuery {
products(first: 10) {
nodes {
not_a_field
}
}
}`,
);
return json({ data: await response.json() });
} catch (error) {
if (error instanceof GraphqlQueryError) {
// error.body.errors:
// { graphQLErrors: [
// { message: "Field 'not_a_field' doesn't exist on type 'Product'" }
// ] }
return json({ errors: error.body?.errors }, { status: 500 });
}
return json({ message: "An error occurred" }, { status: 500 });
}
}
import { shopifyApp } from "@shopify/shopify-app-remix/server";
const shopify = shopifyApp({
// ...
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const {
admin,
session,
} = await authenticate.admin(request);
return json(
admin.rest.resources.Order.count({ session }),
);
};
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;
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
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 });
};
import { 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;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const {
admin,
session,
} = await authenticate.admin(request);
const response = admin.rest.post({
path: "customers/7392136888625/send_invite.json",
body: {
customer_invite: {
to: "new_test_email@shopify.com",
from: "j.limited@example.com",
bcc: ["j.limited@example.com"],
subject: "Welcome to my new shop",
custom_message: "My awesome new store",
},
},
});
const customerInvite = await response.json();
return json({ customerInvite });
};
import { 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;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
const billingCheck = await billing.require({
plans: [MONTHLY_PLAN],
onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),
});
const subscription = billingCheck.appSubscriptions[0];
const cancelledSubscription = await billing.cancel({
subscriptionId: subscription.id,
isTest: true,
prorate: true,
});
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
export const ANNUAL_PLAN = 'Annual subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
[ANNUAL_PLAN]: {
lineItems: [
{
amount: 50,
currencyCode: 'USD',
interval: BillingInterval.Annual,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
const { hasActivePayment, appSubscriptions } = await billing.check({
plans: [MONTHLY_PLAN],
isTest: false,
});
console.log(hasActivePayment);
console.log(appSubscriptions);
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
export const ANNUAL_PLAN = 'Annual subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
[ANNUAL_PLAN]: {
lineItems: [
{
amount: 50,
currencyCode: 'USD',
interval: BillingInterval.Annual,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
const { hasActivePayment, appSubscriptions } = await billing.check();
// This will be true if any payment is found
console.log(hasActivePayment);
console.log(appSubscriptions);
};
import { ActionFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { billing } = await authenticate.admin(request);
const chargeBilling = await billing.createUsageRecord({
description: "Usage record for product creation",
price: {
amount: 1,
currencyCode: "USD",
},
isTest: true,
});
console.log(chargeBilling);
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const USAGE_PLAN = 'Usage subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[USAGE_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Usage,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
await billing.require({
plans: [MONTHLY_PLAN],
onFailure: async () => billing.request({
plan: MONTHLY_PLAN,
isTest: true,
returnUrl: 'https://admin.shopify.com/store/my-store/apps/my-app/billing-page',
}),
});
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
export const ANNUAL_PLAN = 'Annual subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
[ANNUAL_PLAN]: {
lineItems: [
{
amount: 50,
currencyCode: 'USD',
interval: BillingInterval.Annual,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
await billing.require({
plans: [MONTHLY_PLAN],
onFailure: async () => billing.request({
plan: MONTHLY_PLAN,
isTest: true,
trialDays: 14,
lineItems: [
{
interval: BillingInterval.Every30Days,
discount: { value: { percentage: 0.1 } },
},
],
}),
});
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
export const ANNUAL_PLAN = 'Annual subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
[ANNUAL_PLAN]: {
lineItems: [
{
amount: 50,
currencyCode: 'USD',
interval: BillingInterval.Annual,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
await billing.require({
plans: [MONTHLY_PLAN],
isTest: true,
onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),
});
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { LoaderFunctionArgs, redirect } from "@remix-run/node";
import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { billing } = await authenticate.admin(request);
const billingCheck = await billing.require({
plans: [MONTHLY_PLAN, ANNUAL_PLAN],
isTest: true,
onFailure: () => redirect('/select-plan'),
});
const subscription = billingCheck.appSubscriptions[0];
console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const MONTHLY_PLAN = 'Monthly subscription';
export const ANNUAL_PLAN = 'Annual subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[MONTHLY_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Every30Days,
}
],
},
[ANNUAL_PLAN]: {
lineItems: [
{
amount: 50,
currencyCode: 'USD',
interval: BillingInterval.Annual,
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;
import { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { billing } = await authenticate.admin(request);
await billing.updateUsageCappedAmount({
subscriptionLineItemId: "gid://shopify/AppSubscriptionLineItem/12345?v=1&index=1",
cappedAmount: {
amount: 10,
currencyCode: "USD"
},
});
// App logic
};
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
export const USAGE_PLAN = 'Usage subscription';
const shopify = shopifyApp({
// ...etc
billing: {
[USAGE_PLAN]: {
lineItems: [
{
amount: 5,
currencyCode: 'USD',
interval: BillingInterval.Usage,
terms: "Usage based"
}
],
},
}
});
export default shopify;
export const authenticate = shopify.authenticate;