import '@shopify/shopify-api/adapters/web-api';
import {
ConfigInterface as ApiConfig,
ShopifyError,
shopifyApi,
} from '@shopify/shopify-api';
import {SessionStorage} from '@shopify/shopify-app-session-storage';
import {type AppConfig, type AppConfigArg} from './config-types';
import {
AppDistribution,
type BasicParams,
type ShopifyApp,
type ShopifyAppBase,
type AdminApp,
type SingleMerchantApp,
type AppStoreApp,
} from './types';
import {SHOPIFY_REACT_ROUTER_LIBRARY_VERSION} from './version';
import {registerWebhooksFactory} from './authenticate/webhooks';
import {authStrategyFactory} from './authenticate/admin/authenticate';
import {authenticateWebhookFactory} from './authenticate/webhooks/authenticate';
import {overrideLogger} from './override-logger';
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';
import {createTokenExchangeStrategy} from './authenticate/admin/strategies/token-exchange';
import {createMerchantCustomAuthStrategy} from './authenticate/admin/strategies/merchant-custom-app';
import {IdempotentPromiseHandler} from './authenticate/helpers/idempotent-promise-handler';
import {authenticateFlowFactory} from './authenticate/flow/authenticate';
import {authenticateFulfillmentServiceFactory} from './authenticate/fulfillment-service/authenticate';
import {authenticatePOSFactory} from './authenticate/pos/authenticate';
import {FutureFlagOptions, logDisabledFutureFlags} from './future/flags';
/**
* Creates an object your app will use to interact with Shopify.
*
* @param appConfig Configuration options for your Shopify app, such as the scopes your app needs.
* @returns `ShopifyApp` An object constructed using your appConfig. It has methods for interacting with Shopify.
*
* @example
*
The minimum viable configuration
* ```ts
* // /shopify.server.ts
* import { shopifyApp } from "@shopify/shopify-app-react-router/server";
*
* const shopify = shopifyApp({
* apiKey: process.env.SHOPIFY_API_KEY!,
* apiSecretKey: process.env.SHOPIFY_API_SECRET!,
* scopes: process.env.SCOPES?.split(",")!,
* appUrl: process.env.SHOPIFY_APP_URL!,
* });
* export default shopify;
* ```
*/
export function shopifyApp<
Config extends AppConfigArg,
Storage extends SessionStorage,
Future extends FutureFlagOptions = Config['future'],
>(appConfig: Readonly): ShopifyApp {
const api = deriveApi(appConfig);
const config = deriveConfig(appConfig, api.config);
const logger = overrideLogger(api.logger);
if (appConfig.webhooks) {
api.webhooks.addHandlers(appConfig.webhooks);
}
const params: BasicParams = {api, config, logger};
let strategy;
if (config.distribution === AppDistribution.ShopifyAdmin) {
strategy = createMerchantCustomAuthStrategy(params);
} else {
strategy = createTokenExchangeStrategy(params);
}
const authStrategy = authStrategyFactory({
...params,
strategy,
});
const shopify:
| AdminApp
| AppStoreApp
| SingleMerchantApp = {
sessionStorage: config.sessionStorage,
addDocumentResponseHeaders: addDocumentResponseHeadersFactory(params),
registerWebhooks: registerWebhooksFactory(params),
authenticate: {
admin: authStrategy,
flow: authenticateFlowFactory(params),
fulfillmentService: authenticateFulfillmentServiceFactory(params),
pos: authenticatePOSFactory(params),
public: authenticatePublicFactory(params),
webhook: authenticateWebhookFactory(params),
},
unauthenticated: {
admin: unauthenticatedAdminContextFactory(params),
storefront: unauthenticatedStorefrontContextFactory(params),
},
};
if (
isAppStoreApp(shopify, appConfig) ||
isSingleMerchantApp(shopify, appConfig)
) {
shopify.login = loginFactory(params);
}
logDisabledFutureFlags(config, logger);
return shopify as ShopifyApp;
}
function isAppStoreApp(
_shopify: ShopifyAppBase,
config: Config,
): _shopify is AppStoreApp {
return config.distribution === AppDistribution.AppStore;
}
function isSingleMerchantApp(
_shopify: ShopifyAppBase,
config: Config,
): _shopify is SingleMerchantApp {
return config.distribution === AppDistribution.SingleMerchant;
}
// This function is only exported so we can unit test it without having to mock the underlying module.
// It's not available to consumers of the library because it is not exported in the index module, and never should be.
export function deriveApi(appConfig: AppConfigArg): BasicParams['api'] {
let appUrl: URL;
try {
appUrl = new URL(appConfig.appUrl);
} catch (error) {
const message =
appConfig.appUrl === ''
? `Detected an empty appUrl configuration, please make sure to set the necessary environment variables.\n` +
`If you're deploying your app, you can find more information at https://shopify.dev/docs/apps/launch/deployment/deploy-web-app/deploy-to-hosting-service#step-4-set-up-environment-variables`
: `Invalid appUrl configuration '${appConfig.appUrl}', please provide a valid URL.`;
throw new ShopifyError(message);
}
/* eslint-disable no-process-env */
if (appUrl.hostname === 'localhost' && !appUrl.port && process.env.PORT) {
appUrl.port = process.env.PORT;
}
/* eslint-enable no-process-env */
appConfig.appUrl = appUrl.origin;
let userAgentPrefix = `Shopify React Router Library v${SHOPIFY_REACT_ROUTER_LIBRARY_VERSION}`;
if (appConfig.userAgentPrefix) {
userAgentPrefix = `${appConfig.userAgentPrefix} | ${userAgentPrefix}`;
}
return shopifyApi({
...appConfig,
hostName: appUrl.host,
hostScheme: appUrl.protocol.replace(':', '') as 'http' | 'https',
userAgentPrefix,
isEmbeddedApp: true,
isCustomStoreApp: appConfig.distribution === AppDistribution.ShopifyAdmin,
billing: appConfig.billing,
future: {
unstable_managedPricingSupport: true,
},
_logDisabledFutureFlags: false,
});
}
function deriveConfig(
appConfig: AppConfigArg,
apiConfig: ApiConfig,
): AppConfig {
if (
!appConfig.sessionStorage &&
appConfig.distribution !== AppDistribution.ShopifyAdmin
) {
throw new ShopifyError(
'Please provide a valid session storage. Refer to https://github.com/Shopify/shopify-app-js/blob/main/README.md#session-storage-options for options.',
);
}
const authPathPrefix = appConfig.authPathPrefix || '/auth';
appConfig.distribution = appConfig.distribution ?? AppDistribution.AppStore;
return {
...appConfig,
...apiConfig,
billing: appConfig.billing,
scopes: apiConfig.scopes,
idempotentPromiseHandler: new IdempotentPromiseHandler(),
canUseLoginForm: appConfig.distribution !== AppDistribution.ShopifyAdmin,
useOnlineTokens: appConfig.useOnlineTokens ?? false,
hooks: appConfig.hooks ?? {},
sessionStorage: appConfig.sessionStorage as Storage,
future: appConfig.future ?? {},
auth: {
path: authPathPrefix,
callbackPath: `${authPathPrefix}/callback`,
patchSessionTokenPath: `${authPathPrefix}/session-token`,
exitIframePath: `${authPathPrefix}/exit-iframe`,
loginPath: `${authPathPrefix}/login`,
},
distribution: appConfig.distribution,
};
}