Remove docs from the repo

This commit is contained in:
Lukasz Ostrowski 2023-08-16 09:14:37 +02:00
parent faccdc1299
commit 53f70180a6
10 changed files with 0 additions and 1252 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1 +0,0 @@
* @saleor/docs

View file

@ -1,145 +0,0 @@
# Api Handlers
Saleor Apps are meant to work in serverless environment, where Cloud Functions are the foundations of server-side code.
Currently, Saleor heavily relies on Next.js, but in the future, other platforms will be supported.
## Required handlers
Saleor requires 2 endpoints to be available for a standalone app:
- Manifest endpoint - Returns JSON object with app properties, like its name or permissions. [Read more](https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest)
- Register endpoint - During the installation process, Saleor sends `POST` request with auth token to this endpoint. [Read more](https://docs.saleor.io/docs/3.x/developer/extending/apps/installing-apps#installation-using-graphql-api)
## Api handlers built-in SDK
To hide Saleor internal logic, app-sdk provides handlers factories. They should work with minimal configuration, leaving
App creators space for domain logic.
### Manifest handler factory
Example usage of manifest handler in Next.js
```typescript
// pages/api/manifest.ts
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
export default createManifestHandler({
manifestFactory({ request, appBaseUrl }) {
return {
name: "My Saleor App",
tokenTargetUrl: `${appBaseUrl}/api/register`,
appUrl: appBaseUrl,
permissions: [],
id: "my-saleor-app",
version: "1",
};
},
});
```
Options provided to handler factory
```typescript
type CreateManifestHandlerOptions = {
manifestFactory(context: { appBaseUrl: string; request: NextApiRequest }): AppManifest;
};
```
You can use NextApiRequest to read additional params from request. For example read Saleor version to enable or disabled some features, depending on their support.
See [source](./src/handlers/next/create-manifest-handler.ts) for more details. See [manifest](../src/types.ts) too.
### App register handler factory
Example usage of app register handler in Next.js
```typescript
// pages/api/register.ts
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
import { UpstashAPL } from "@saleor/app-sdk/APL";
export default createAppRegisterHandler({
apl: new UpstashAPL({
restURL: "...",
restToken: "...",
}),
allowedSaleorUrls: ["https://your-saleor.saleor.cloud/graphql/"], // optional, see options below
async onRequestVerified(req, { authData, respondWithError }) {
await doSomethingAndBlockInstallation(authData.token).catch((err) => {
// Return this method to break installation flow and show error in the Dashboard
return respondWithError({ message: "Error, installation will fail" });
});
},
});
```
Options provided to handler factory
```typescript
export type CreateAppRegisterHandlerOptions = {
apl: APL;
/**
* Provide your Saleor /graphql/ endpoints (or functions),
* to allow app registration only in allowed Saleor instances.
*/
allowedSaleorUrls?: Array<string | ((saleorApiUrl: string) => boolean)>;
/**
* Optional
* Run right after Saleor calls this endpoint
*/
onRequestStart?(
request: Request,
context: {
authToken?: string;
saleorDomain?: string;
saleorApiUrl?: string;
respondWithError: ({ status, message }) => never; // will throw
}
): Promise<void>;
/**
* Optional
* Run after all security checks
*/
onRequestVerified?(
request: Request,
context: {
authData: AuthData;
respondWithError: ({ status, message }) => never; // will throw
}
): Promise<void>;
/**
* Optional
* Run after APL successfully AuthData, assuming that APL.set will reject a Promise in case of error
*/
onAuthAplSaved?(
request: Request,
context: {
authData: AuthData;
respondWithError: ({ status, message }) => never; // will throw
}
): Promise<void>;
/**
* Optional
* Run after APL fails to set AuthData
*/
onAplSetFailed?(
request: Request,
context: {
authData: AuthData;
error: unknown;
respondWithError: ({ status, message }) => never; // will throw
}
): Promise<void>;
};
```
See [APL](./apl.md) for details what is Auth Persistence Layer in Saleor apps
### Async Webhook Handler
App SDK provides a utility that helps building (async) webhook handlers, so app can react on Saleor events.
Read about it [here](./saleor-async-webhook.md).

View file

@ -1,255 +0,0 @@
# APL - Auth Persistence Layer
APL is an interface for managing auth data of registered Apps. Implementing it does not restrict you from choosing the right technology for your project (for example Redis, Postgres, S3, etc.), and provides a common set of operations which can be used by your application and functions provided by App SDK.
## Available methods
- `get: (saleorApiUrl: string) => Promise<AuthData | undefined>` - If the entry for given saleorApiUrl exists, returns AuthData object.
- `set: (authData: AuthData) => Promise<void>` - Save auth data.
- `delete: (saleorApiUrl: string) => Promise<void>` - Remove auth data fot the given API URL.
- `getAll: () => Promise<AuthData[]>` - Returns all auth data available.
- `isReady: () => Promise<AplReadyResult>` - Check if persistence layer behind APL is ready. For example: database connection established
- `isConfigured: () => Promise<AplConfiguredResult>` - Check if persistence layer behind APL is configured. For example: env variable required by database connection
## AuthData
Interface containing data used for communication with the Saleor API:
```ts
export interface AuthData {
domain: string;
token: string;
saleorApiUrl: string;
appId: string;
jwks: string;
}
```
- `domain` - Domain of the API
- `token` - Authorization token
- `saleorApiUrl` - Full URL to the Saleor GraphQL API
- `appID` - ID of the app assigned during the installation process
- `jwks` - JSON Web Key Set available at `https://<your-saleor-domain>/.well-known/jwks.json`, cached in the APL for the faster webhook validation
## AplReadyResult & ApConfiguredResult
Responses from `isReady()` and `isConfigured()` should match following:
```ts
type AplReadyResult =
| {
ready: true;
}
| {
ready: false;
error: Error;
};
type AplConfiguredResult =
| {
configured: true;
}
| {
configured: false;
error: Error;
};
```
## Example implementation
Let's create an APL, which uses Redis for data storage:
```ts
import { createClient } from "redis";
import { APL, AuthData } from "@saleor/app-sdk/apl";
const client = createClient();
await client.connect();
const redisAPL: APL = {
get: async (saleorApiUrl: string) => {
const response = await client.get(saleorApiUrl);
if (response) {
return JSON.parse(response);
}
return;
},
set: async (authData: AuthData) => {
await client.set(authData.saleorApiUrl, JSON.stringify(authData));
},
delete: async (saleorApiUrl: string) => {
await client.del(saleorApiUrl);
},
getAll: async () => {
throw new Exception("Not implemented.");
},
};
```
You'll be able to use it directly:
```ts
import { redisAPL } from "./apl";
const getSavedAuthData = async () => {
await redisAPL.get("https://demo.saleor.io/graphql/");
};
```
And with middleware from the SDK:
```ts
import { withRegisteredSaleorDomainHeader } from "@saleor/app-sdk/middleware";
import { redisAPL } from "./apl";
const handler = async (request) => {
return Response.OK({ message: "If you see this, your app is registered!" });
};
// the middleware will reject request if it's domain has not been registered
export default withRegisteredSaleorDomainHeader({ apl: redisAPL })(handler);
```
### Using different APL depending on the environment
Depending on the environment your app is working on, you may want to use a different APL. For example during local development you might like to use `FileAPL`, because it does not require any additional infrastructure. Deployed apps on the other hand need more robust solution.
To handle both scenarios, initialize the proper APLs in your code based on it's environment. In your application code:
```ts
// lib/saleorApp.ts
import { FileAPL, UpstashAPL } from "@saleor/app-sdk/APL";
// Based on environment variable the app will use a different APL:
// - For local development store auth data in the `.auth-data.json`.
// - For app deployment on hosted environment like Vercel, use UpstashAPL
export const apl = process.env.APL === "upstash" ? new UpstashAPL() : new FileAPL();
```
Now you can use it for in your view:
```ts
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { withRegisteredSaleorDomainHeader } from "@saleor/app-sdk/middleware";
import { NextApiHandler } from "next";
// import created APL
import { apl } from "@lib/saleorApp";
const handler: NextApiHandler = async (request, response) => {
const saleorApiUrl = request.headers[SALEOR_API_URL_HEADER];
// Get auth data
const authData = apl.get(saleorApiUrl);
// view logic...
return response.status(200).end();
};
export default handler;
```
## Available APLs
### FileAPL
File based storage of auth data, intended for local development. Data are stored in the `.saleor-app-auth.json` file. You'll be able to develop app without additional dependencies or infrastructure.
Please note: this APL supports single tenant only (new registrations overwrite previous ones) and should not be used on production.
### UpstashAPL
[Upstash](https://upstash.com) is a Redis SaaS targeting serverless applications. It's free tier is more than enough to start developing multi-tenant Saleor Apps, and credit card info is not required to create an account.
APL implementation use REST endpoints to store the data, so no extra dependencies are required.
To use UpstashAPL, you'll need endpoint URL and password. Both will be available at [dashboard](https://console.upstash.com/) after creating a database. Credentials can be passed to APL during it's creation:
```ts
import { UpstashAPL } from "@saleor/app-sdk/APL";
const apl = new UpstashAPL({
restToken: "XXXXXXXXXXXXXX",
restURL: "https://eu2-red-panda-00000.upstash.io",
});
```
Or using environment variables: `UPSTASH_TOKEN`, `UPSTASH_URL`.
Note: Recently Vercel introduced [KV storage](https://vercel.com/docs/storage/vercel-kv) which uses Upstash under the hood. Upstash APL will work with values provided by Vercel KV Storage too.
### EnvAPL
For very simple, static & single-tenant applications, you may want to just set everything once, usually in env variables and just leave it.
There is a simple APL for this use case - `EnvAPL`
> **Warning**
> Using this APL is highly discouraged in any production environment.
> It will break if app token is regenerated, it will not work with any flow related to updates of the app.
#### EnvAPL usage
##### 1. Configure app to use env apl
```tsx
import { EnvAPL } from "@saleor/app-sdk/APL";
const apl = new EnvAPL({
env: {
/**
* Map your env variables here. You dont have these values yet
*/
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
/**
* Set it to "true" - during app registration check you app logs. APL will print values you need
*/
printAuthDataOnRegister: true,
});
```
##### 2. Set env variables
After step 1, you should see your logs in similar way:
```
┌──────────────┬─────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────┼─────────────────────────────────────────────┤
│ saleorApiUrl │ 'https://my-saleor-instance.cloud/graphql/' │
│ appId │ 'app-id' │
│ token │ 'some-token' │
│ jwks │ '{}' │
│ domain │ 'my-saleor-instance.cloud' │
└──────────────┴─────────────────────────────────────────────┘
```
You need to se env variables in your provider, eg. in Vercel - you need appId, token and saleorApiUrl.
Ensure name of variables match your step 1 constructor options
##### 3. Turn off logging and redeploy
After env is set, you should disable printing it for security reasons
Change constructor to include
```
printAuthDataOnRegister: false
```
Then, redeploy the app. It should be configured and work for this specific Saleor instance
Keep in mind that
- If you promote environment, you need to update saleorApiUrl
- If you reinstall the app, you need to repeat the process to receive new token and ID

View file

@ -1,189 +0,0 @@
# AppBridge
App bridge is an interface that connects App (running inside Dashboard) with Dashboard itself.
## Setup
Create instance of AppBridge by running following code
```js
import { AppBridge } from "@saleor/app-sdk/app-bridge";
const appBridge = new AppBridge(options);
```
Options object is following:
```
type AppBridgeOptions = {
targetDomain?: string;
saleorApiUrl?: string;
initialLocale?: LocaleCode;
autoNotifyReady?: boolean;
initialTheme?: "dark" | "light"
};
```
## Access app state:
```js
const { token, saleorApiUrl, ready, id } = appBridge.getState();
```
Available state represents `AppBridgeState`:
```typescript
type AppBridgeState = {
token?: string;
id: string;
ready: boolean;
domain: string;
path: string;
theme: ThemeType;
locale: LocaleCode; // See src/locales.ts
/**
* Full URL including protocol and path where GraphQL API is available
**/
saleorApiUrl: string;
/**
* Versions of Saleor that app is being installed. Available from 3.15.
*/
saleorVersion?: string;
dashboardVersion?: string;
user?: {
/**
* Original permissions of the user that is using the app.
* *Not* the same permissions as the app itself.
*
* Can be used by app to check if user is authorized to perform
* domain specific actions
*/
permissions: Permission[];
email: string;
};
/**
* Permissions of the app itself
*/
appPermissions?: AppPermission[];
};
```
## AppBridgeProvider
`AppBridgeProvider` and `useAppBridge` hook are exposed from app-sdk
```tsx
// app.tsx
import { AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
<AppBridgeProvider>
<YourApp />
</AppBridgeProvider>;
```
`AppBridgeProvider` can optionally receive AppBridge instance in props, otherwise will create one automatically
### useAppBridge hook
In components wrapped with `AppBridgeProvider`, `useAppBridge` hook is available
```tsx
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";
const MyComponent = () => {
const { appBridge, appBridgeState } = useAppBridge();
useEffect(() => {
appBridge?.dispatch(/* Something */);
}, [appBridge]);
return <div>Current locale is: {appBridgeState?.locale}</div>;
};
```
`appBridgeState?` and `appBridge` can be nullish, because in server side context it's not available
## Events
Events are messages that originate in Saleor Dashboard. AppBridge can subscribe on events and app can react on them
### Subscribing to events
`subscribe(eventType, callback)` - can be used to listen to particular [event type](#available-event-types). It returns an unsubscribe function, which unregisters the callback.
Example:
```typescript
const unsubscribe = appBridge.subscribe("handshake", (payload) => {
setToken(payload.token); // do something with event payload
const { token } = appState.getState(); // you can also get app's current state here
});
// unsubscribe when callback is no longer needed
unsubscribe();
```
### Unsubscribing multiple listeners
`unsubscribeAll(eventType?)` - unregisters all callbacks of provided type. If no type was provided, it will remove all event callbacks.
Example:
```js
// unsubscribe from all handshake events
appBridge.unsubscribeAll("handshake");
// unsubscribe from all events
appBridge.unsubscribeAll();
```
### Available event types
| Event type | Description |
| :-------------- | :--------------------------------------------------------------------------- |
| `handshake` | Fired when iFrame containing the App is initialized or new token is assigned |
| `response` | Fired when Dashboard responds to an Action |
| `redirect` | Fired when Dashboard changes a subpath within the app path |
| `theme` | Fired when Dashboard changes the theme |
| `localeChanged` | Fired when Dashboard changes locale (and passes locale code in payload) |
| `tokenRefresh` | Fired when Dashboard receives a new auth token and passes it to the app |
See [source code for detailed payload](./src/app-bridge/events.ts)
## Actions
Actions expose a high-level API to communicate with Saleor Dashboard. They're exported under an `actions` namespace.
### Available methods
**`dispatch(action)`** - dispatches an Action. Returns a promise which resolves when action is successfully completed.
Example:
```js
import { actions } from "@saleor/app-sdk/app-bridge";
const handleRedirect = async () => {
await appBridge.dispatch(actions.Redirect({ to: "/orders" }));
console.log("Redirect complete!");
};
handleRedirect();
```
### Available actions
| Action | Arguments | Description |
| :---------------------------- | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Redirect` | `to` (string) - relative (inside Dashboard) or absolute URL path | |
| | `newContext` (boolean) - should open in a new browsing context | |
| `Notification` | `status` (`info` / `success` / `warning` / `error` / undefined) | |
| | `title` (string / undefined) - title of the notification | |
| | `text` (string / undefined) - content of the notification | |
| | `apiMessage` (string / undefined) - error log from api | |
| `NotifyReady` | | Inform Dashboard that AppBridge is ready |
| `UpdateRouting` | `newRoute` - current path of App to be set in URL | |
| `RequestPermissions` (>=3.15) | `permissions` - array of AppPermission, `redirectPath` - path app will be redirected to after operation ends | Ask Dashboard to give more permissions to the app. Dashboard will unmount app. After user approves or denies, Dashboard will redirect to `redirectPath`. If operation fails, `?error=REASON` will be appended |

View file

@ -1,54 +0,0 @@
# Debugging
The ability to debug is important for app developers.
App SDK provides opt-in verbose logs, that can be enabled for specific modules.
Debug logs are implemented with [debug](https://www.npmjs.com/package/debug) package and work both in Node and Browser.
## Enabling verbose logs
Debug logs are aggregated within [namespaces](#namespaces). They can be enabled, disabled, or configured.
By default, all verbose logs are disabled.
To enable logs, use the following:
### Enabling in Node context
Add `DEBUG=app-sdk:{namespaces}` env variable.
#### Example
```shell
DEBUG=* pnpm run dev
```
For more details check [debug package](https://github.com/debug-js/debug#usage).
### Enabling in Browser context
Set `localStorage.debug = 'app-sdk:{namepsaces}'` in your frontend code. For more details see [debug package](https://github.com/debug-js/debug#browser-support).
#### Debug in iframe
Note, that Saleor Apps are usually hosted on a different domain than Saleor Dashboard.
That means `localStorage` must be called in the context of the App's URL, not Saleor Dashboard.
#### Example
```javascript
// In devtools console
localStorage.debug = "*";
```
## Namespaces
Use the namespace name to enable debug logs for each module.
| Namespace name | Description |
| ----------------------------- | -------------------------------------------------- |
| \app-sdk:\* | Enable all |
| app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) |
| app-sdk:Middleware:\* | Enable all middlewares (node only) |
| app-sdk:APL:\* | Enable all APLs (node only) |
| app-sdk:SaleorAsyncWebhook:\* | Enable SaleorAsyncWebhook utility |

View file

@ -1,101 +0,0 @@
# Protected API handlers
The App SDK provides helpers which ensure, that incoming requests are sent from Saleor dashboard.
Example of such situation could be a change of the application configuration iframe.
> **Warning**
> This handler only works for requests originated from frontend calls. It checks JWT token from the client, which is available
> in AppBridge. Do not call this endpoint from backend context (calling it from API function will fail)
## How to protect the endpoint
First, create handler for your business logic. The only difference from usual NextJS handler is an additional argument `ctx` of type `ProtectedHandlerContext`, which contains additional data related to the request:
```typescript
export type ProtectedHandlerContext = {
baseUrl: string; // the URL your application is available
authData: AuthData; // Auth Data which can be used to communicate with the Saleor API
user: {
email: string;
userPermissions: string[];
};
};
```
`createProtectedHandler` will check if:
- the request has `saleor-api-url` header of the Saleor instance
- the API URL has been registered, with help of the APL
- the request has `authorization-bearer`
- the auth token is a valid JWT token created by the Saleor running on the given URL
- user has required permissions in the token
For example purposes our endpoint will only log welcome message:
```typescript
import { createProtectedHandler, ProtectedHandlerContext } from "@saleor/app-sdk/handlers/next";
import { NextApiRequest, NextApiResponse } from "next";
import { saleorApp } from "../../../saleor-app";
export const handler = async (
req: NextApiRequest,
res: NextApiResponse,
ctx: ProtectedHandlerContext
) => {
console.log(`Greetings from ${ctx.authData.domain}`);
res.status(200);
};
/**
* If any of the requirements is failed, an error response will be returned.
* Otherwise, provided handler function fill be called.
*
* Last argument is optional array of permissions that will be checked. If user doesn't have them, will return 401 before handler is called
*/
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_ORDERS"]);
```
To make your requests successfully communicate with the backend, `saleor-api-url` and `authorization-bearer` headers are required:
```typescript
fetch("/api/protected", {
headers: {
/**
* Both API URL and token are available in the appBridgeState. Based on those
* headers the backend will check if the request has enough permissions to
* perform the action.
*/
"saleor-api-url": saleorApiUrl,
"authorization-bearer": token,
},
});
```
If you want to read more about `appBridgeState`, check [App Bridge](./app-bridge.md) documentation.
### Using `useAuthenticatedFetch()` hook
Instead of manually attaching headers with AppBridge context, you can use `useAuthenticatedFetch()` hook
Since it requires AppBridge, it's only available in browser context. It depends on `Window` object,
so your app will break if Next.js tries to render it server-side. Hence, ensure component that uses the hook is imported with dynamic()
Component must be within `AppBridgeProvider` to have access to the AppBridge
```tsx
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";
export const ClientComponent = () => {
const fetch = useAuthenticatedFetch();
useEffect(() => {
/**
* Auth headers are set up automatically, so you can just call the fetch function
*/
fetch("/api/protected");
}, [fetch]);
return <div>Your UI</div>;
};
```

View file

@ -1,59 +0,0 @@
# App protected views
Most likely at least some screens of the Saleor App are meant to be visible only withing Dashboard "Apps" section.
SDK provides a Higher Order Component that can be used to wrap Next.js pages and perform basic protection checks
## Example usage
```tsx
import { withAuthorization } from "@saleor/app-sdk/app-bridge";
const AppConfigurationPage: NextPage = () => {
return <div>This is app inside Dashboard</div>;
};
export default withAuthorization()(AppConfigurationPage);
```
This code will render default, unstyled error screens if one of following checks are not met:
- App is rendered Server Side
- App is rendered outside iframe
- App is rendered and token can't be accessed
- App is rendered, but token is invalid
If all conditions are successful, child Page will be rendered
## Configuration
Each condition can be styled with custom error React Node, by passing configuration into HOC factory
Interface of configuration is following
```typescript
type Props = {
unmounted?: ReactNode;
notIframe?: ReactNode;
noDashboardToken?: ReactNode;
dashboardTokenInvalid?: ReactNode;
};
```
Each prop is optional, so some of them can be left with default fallback component.
### Full example
```tsx
import { withAuthorization } from "@saleor/app-sdk/app-bridge";
import { ErrorPage } from "src/error-page";
const AppConfigurationPage: NextPage = () => {
return <div>This is app inside Dashboard</div>;
};
export default withAuthorization({
unmounted: null,
notIframe: <ErrorPage error="403">This page can be accessed from dashboard</ErrorPage>,
})(AppConfigurationPage);
```

View file

@ -1,341 +0,0 @@
# Saleor Webhook utilities
Apps are usually connected via webhooks - one App sends an HTTP request to another App, informing about some event or requesting some action to be performed.
To inform your App about events originated from Saleor, you need to expose a webhook handler, which Saleor will call with POST request.
The App SDK provides a utility that abstracts connection details and auth, allowing developers to focus on business logic.
Depending on the type of the webhook, you can choose one of the classes:
- `SaleorAsyncWebhook`
- `SaleorSyncWebhook`
## Common configuration
Both `SaleorSyncWebhook` and `SaleorAsyncWebhook` contain similar API with little differences.
### Constructing Webhook instance
In Next.js pages create a page, e.g. `pages/api/webhooks/order-created.ts`. We recommend to keep webhook type in file name, which will be resolved by Next.js to route path.
```typescript
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
/**
* Default body parser must be turned off - raw body is needed to verify signature
* /
export const config = {
api: {
bodyParser: false,
},
};
/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type OrderPayload = {
id: string;
};
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>(options);
```
For `SaleorSyncWebhook` it will be similar. Create e.g. `order-calculate-taxes.ts` page and create a new instance:
```typescript
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";
/**
* Default body parser must be turned off - raw body is needed to verify signature
* /
export const config = {
api: {
bodyParser: false,
},
};
/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type CalculateTaxedPayload = {
// taxes payload from subscription
};
export const orderCalculateTaxesWebhook = new SaleorSyncWebhook<OrderPayload>(options);
```
### Configuring Webhook instance
`options` in constructor must be specified. Here are all options:
```typescript
type Options = {
/**
* Additional webhook name, optional.
*/
name?: string;
/**
* Path to webhook. Should represent relative path from base app URL. In Next.js it will start with `api/`, e.g. `api/webhooks/order-created`.
*/
webhookPath: string;
/**
* Valid Async or Sync webhook. Constructor is statically typed so only valid Sync/Async webhooks will be allowed
*/
event: Event;
/**
* Should event be active after installation, enabled by default
*/
isActive?: boolean;
/**
* APL instance - see docs/apl/md
*/
apl: APL;
/**
* Optional callback that allows to inject custom error handling and take control of the response
*/
onError?(error: WebhookError | Error, req: NextApiRequest, res: NextApiResponse): void;
/**
* Optional callback that allows to format error message. Useful to control how much details should be returned in the response
*/
formatErrorResponse?(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
): Promise<{
code: number;
body: object | string;
}>;
/**
* Required subscription query. Can be raw GraphQL string or the instance of query wrapped in `gql` tags
*/
query: string | ASTNode;
};
```
#### Configuration examples
##### SaleorAsyncWebhook configuration example
```typescript
// pages/api/webhooks/order-created.ts
/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type OrderPayload = {
id: string;
};
/**
* Default body parser must be turned off - raw body is needed to verify signature
* /
export const config = {
api: {
bodyParser: false,
},
};
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>({
name: "Order Created",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CREATED",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on OrderCreated {
order {
id
}
}
}
}
`,
onError(error: WebhookError | Error) {
// Can be used to e.g. trace errors
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
```
##### SyncAsyncWebhook configuration example
```typescript
// pages/api/webhooks/order-created.ts
/**
* To be type safe, define payload from API. This should be imported from generated graphQL code
*/
type Payload = {
taxBase: {
currency: string;
};
};
/**
* Default body parser must be turned off - raw body is needed to verify signature
* /
export const config = {
api: {
bodyParser: false,
},
};
export const orderCalculateTaxesWebhook = new SaleorAsyncWebhook<Payload>({
name: "Order Calculate Taxes",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CALCULATE_TAXES",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on CalculateTaxes {
taxBase {
currency
}
}
}
}
`,
onError(error: WebhookError | Error) {
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
```
### Resources
- Check available events [here](https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#available-webhook-events)
- [Read more about APLs](./apl.md)
- [Subscription query documentation](https://docs.saleor.io/docs/3.x/developer/extending/apps/subscription-webhook-payloads)
### Extending app manifest
Webhooks are created in Saleor when the App is installed. Saleor uses [AppManifest](https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest) to get information about webhooks to create.
`SaleorSyncWebhook` & `SaleorAsyncWebhook` utility can generate this manifest:
```typescript
// pages/api/manifest
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { orderCreatedWebhook } from "./order-created.ts";
export default createManifestHandler({
manifestFactory({ appBaseUrl }) {
return {
/**
* Add one or more webhook manifests.
*/
webhooks: [orderCreatedWebhook.getWebhookManifest(appBaseUrl)],
// ...rest of your App's manifest
};
},
});
```
Now, try to read your manifest, in default Next.js config it will be `GET localhost:3000/api/manifest`. You should see webhook configuration as part of manifest response.
### Creating webhook domain logic
Now, let's create a handler that will process webhook data. Let's back to handler file `pages/api/webhooks/order-created.ts`.
```typescript
type OrderPayload = {
id: string;
};
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>({
// ... your configuration
});
/**
* Handler has to be a default export so the Next.js will be able to use it
*/
export default orderCreatedWebhook.createHandler((req, res, context) => {
const { baseUrl, event, payload, authData } = context;
console.log(payload.id); // type is inferred
// Perform some domain logic
// End with status 200
return res.status(200).end();
});
```
### Typed sync webhook response
Sync webhooks need to return response to Saleor, so operation can be completed. To achieve that, `SaleorAsyncWebhook` injects additional context field `buildResponse`.
It infers even from constructor and provides typed factory:
```typescript
const webhook = new SaleorAsyncWebhook({ event: "ORDER_CALCULATE_TAXES" /* ... rest of config */ });
orderCreatedWebhook.createHandler((req, res, context) => {
return res.status(200).send(
context.buildResponse({
// Fields are typed here
})
);
});
```
### query vs subscriptionQueryAst
Subscription query can be specified using plain string or as `ASTNode` object created by `gql` tag.
If your project does not use any code generation for GraphQL operations, use the string. In case you are using [GraphQL Code Generator](https://the-guild.dev/graphql/codegen), which we highly recommend, you should pass a subscription as GraphQL ASTNode:
```typescript
/**
* Subscription query, you can define it in the `.ts` file. If you write operations in separate `.graphql` files, codegen will also export them in the generated file.
*/
export const ExampleProductUpdatedSubscription = gql`
${ProductUpdatedWebhookPayload}
subscription ExampleProductUpdated {
event {
fragment
ProductUpdatedWebhookPayload
on
ProductUpdated {
product {
id
name
}
}
}
}
`;
export const productUpdatedWebhook = new SaleorAsyncWebhook<ProductUpdatedWebhookPayloadFragment>({
name: "Example product updated webhook",
webhookPath: "api/webhooks/saleor/product-updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ExampleProductUpdatedSubscription, // Or use plain string
});
```

View file

@ -1,107 +0,0 @@
# Settings Manager
Settings managers are used to persist configuration data like API keys, preferences, etc..
## `SettingsValue` interface
Entries in the manager are stored using structure:
```
key: string;
value: string;
domain?: string;
```
## `DeleteMetadataArg` interface
Argument that can be used to remove metadata via `manager.delete()` method
It contains key and domain:
```typescript
type DeleteMetadataArg = {
key: string;
domain: string;
};
```
For values which should not be migrated during environment cloning (as private keys to payment provider), developer should use domain field to bind it to particular store instance.
## Available methods
- `get: (key: string, domain?: string) => Promise<string | undefined>`
- `set: (settings: SettingsValue[] | SettingsValue) => Promise<void>`
- `delete: (args: string | string[] | DeleteMetadataArg | DeleteMetadataArg[]) => Promise<void>`
Warning: delete method can throw, if instance of SettingsManager was not configured with proper mutation in constructor.
# MetadataManager
Default manager used by app template. Use app metadata as storage. Since app developer can use any GraphQL client, constructor must be parametrized with fetch and mutate functions:
```ts
import { MetadataEntry } from "@saleor/app-sdk/settings-manager";
import { Client } from "urql";
import {
FetchAppDetailsDocument,
FetchAppDetailsQuery,
UpdateAppMetadataDocument,
DeleteMetadataDocument,
} from "../generated/graphql";
export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]> {
const { error, data } = await client
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
.toPromise();
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
}
export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) {
const { error: mutationError, data: mutationData } = await client
.mutation(UpdateAppMetadataDocument, {
id: appId,
input: metadata,
})
.toPromise();
return (
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
key: md.key,
value: md.value,
})) || []
);
}
export async function deleteMetadata(client: Client, keys: string[]) {
return client.mutation(DeleteMetadataDocument, { keys }).toPromise();
}
```
And create MetadataManager instance:
```ts
const settings = new MetadataManager({
fetchMetadata: () => fetchAllMetadata(client),
mutateMetadata: (md) => mutateMetadata(client, md),
deleteMetadata: (keys) => deleteMetadata(client, keys),
});
```
# EncryptedMetadataManager
This manager encrypts add the layer of encryption for all the stored data.
To operate correctly, the encryption key needs to be passed to the constructor:
```ts
new EncryptedMetadataManager({
encryptionKey: process.env.SECRET_KEY, // secrets should be saved in the environment variables, never in the source code
fetchMetadata: () => fetchAllMetadata(client),
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
deleteMetadata: (keys) => deleteMetadata(client, keys),
});
```
> **Warning**
> If encryption key won't be passed, the application will exit.