Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"turborepo",
"undiscounted",
"żółta",
"noopener"
"noopener",
"Hono"
],
"allowCompoundWords": true,
"useGitignore": true,
Expand Down
43 changes: 30 additions & 13 deletions docs/developer/extending/apps/developing-apps/app-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,83 +17,100 @@ import SendgridIcon from "../../../../../static/img/apps/notification-hub.svg"
## Built by Saleor
These apps were built by Saleor team using [Saleor App Template](/developer/extending/apps/developing-apps/app-template.mdx), however they are not currently available in the App Store and may not maintained. You are can use them as a reference for building your own apps.

<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap'}}>
<CompactCard
name="Invoicing"
description="Example app for invoice generation directly in Saleor Dashboard."
technology="Next.js"
link="https://github.com/saleor/example-app-invoices"
icon={<InvoiceIcon />}
icon={<InvoiceIcon/>}
/>
<CompactCard
name="Stripe"
description="Example implementation of Stripe integration using Transaction API."
technology="Next.js"
link="https://github.com/saleor/saleor-app-payment-stripe"
icon={<StripeIcon />}
/>
icon={<StripeIcon/>}
/>
<CompactCard
name="Klarna"
description="Example implementation of Klarna integration using Transaction API."
technology="Next.js"
link="https://github.com/saleor/saleor-app-payment-klarna"
icon={<img src={KlarnaIcon} style={{padding: "2px"}} />}
icon={<img src={KlarnaIcon} style={{padding: "2px"}}/>}
/>
<CompactCard
name="Dummy Payment App"
description="Bare bones app for testing Saleor Transaction API."
technology="Next.js"
link="https://github.com/saleor/dummy-payment-app"
icon={<DummyPaymentIcon />}
icon={<DummyPaymentIcon/>}
/>
<CompactCard
name="Slack"
description="Example app: connect Saleor to send Slack messages."
technology="Next.js"
link="https://github.com/saleor/example-slack-app"
icon={<SlackIcon style={{padding: "2px"}} />}
icon={<SlackIcon style={{padding: "2px"}}/>}
/>
<CompactCard
name="Authorize.net"
description="Example implementation of Authorize.net integration."
technology="Next.js"
link="https://github.com/saleor/saleor-app-payment-authorize.net"
icon={<img src={AppTemplateIcon} style={{padding: "2px"}} />}
icon={<img src={AppTemplateIcon} style={{padding: "2px"}}/>}
/>
<CompactCard
name="TaxJar"
description="Example app: connect Saleor to calculate taxes with TaxJar."
technology="Next.js"
link="https://github.com/saleor/example-app-taxjar"
icon={<TaxJarIcon style={{padding: "2px"}} />}
icon={<TaxJarIcon style={{padding: "2px"}}/>}
/>
<CompactCard
name="Checkout prices"
description="Example app: set custom prices for checkout lines."
technology="Next.js"
link="https://github.com/saleor/saleor-app-checkout-prices"
icon={<img src={AppTemplateIcon} style={{padding: "2px"}} />}
icon={<img src={AppTemplateIcon} style={{padding: "2px"}}/>}
/>
<CompactCard
name="Mailchimp"
description="Example app: create targeted campaigns using Mailchimp."
technology="Next.js"
link="https://github.com/saleor/example-app-crm/"
icon={<CRMIcon style={{padding: "2px"}} />}
icon={<CRMIcon style={{padding: "2px"}}/>}
/>
<CompactCard
name="Sendgrid"
description="Example app: connect Saleor to Sendgrid."
technology="Next.js"
link="https://github.com/saleor/example-app-sendgrid"
icon={<SendgridIcon style={{padding: "2px"}} />}
icon={<SendgridIcon style={{padding: "2px"}}/>}
/>
<CompactCard
name="Saleor App AWS Lambda"
description="Boilerplate for building Saleor Apps in TypeScript on AWS Lambda with AWS CDK deployment"
technology="TypeScript"
link="https://github.com/saleor/saleor-app-lambda-template"
/>
<CompactCard
name="Saleor App Hono + Deno"
description="Boilterplate for building Saleor Apps in Hono + Deno"
technology="TypeScript, Deno"
link="https://github.com/witoszekdev/saleor-app-hono-deno-template"
/>
<CompactCard
name="Saleor App Hono + Cloudflare Pages"
description="Boilterplate for building Saleor Apps using Hono + Cloudflare Pages / Workers"
technology="TypeScript, Cloudflare Workers"
link="https://github.com/witoszekdev/saleor-app-hono-cf-pages-template"/>
</div>


## Built by the community
These examples can be useful if you want to build a service without the use of the App Template in tech stack other than Next.js.
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap'}}>
<CompactCard
name="AWS Serverless"
description="Implementation of a Saleor app using AWS Lambda"
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: maybe it will be better to have next-api-handlers & aws-api-handlers files?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added examples. I think it's fine to add dedicated pages too. But I suggest to do this in next PRs.

I wanted this PR to contain everything required with v1:

  • Data to be up to date / accurate
  • Migration guide

Maybe in next PRs we can extend docs with additional things?

Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# API Handlers

Saleor Apps are meant to work in a serverless environment, where Cloud Functions are the foundations of server-side code.
App SDK mainly targets serverless environments, like Vercel functions or AWS Lambda. It provides a set of handlers that can be used to build Saleor apps.

Currently, Saleor heavily relies on Next.js, but other platforms will be supported in the future.
SDK providers helpers for following types of functions (that represent API endpoints):
- [Manifest handler](/developer/extending/apps/architecture/app-requirements#manifest-url) - Used to fetch app manifest by Saleor during app installation.
- Register handler - Used to register app in Saleor during installation and save the token.
- Webhook handler - Exposes endpoint that Saleor will call with webhook events.
- Protected handler - Endpoints meant to be allowed only by the App's frontend (from Saleor Dashboard).

## Required handlers

Expand All @@ -23,49 +27,53 @@ Here is an example usage of a manifest handler in Next.js:
```typescript
// pages/api/manifest.ts

// Change "next" to other platforms if needed
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";

export default createManifestHandler({
manifestFactory({ request, appBaseUrl }) {
manifestFactory({ request, appBaseUrl, schemaVersion }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: do we need here schemaVersion? I mean is something we recommend to do with it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to show all available options here (and there are not much). I dont have strong opinion

return {
name: "My Saleor App",
tokenTargetUrl: `${appBaseUrl}/api/register`,
appUrl: appBaseUrl,
permissions: [],
permissions: ["MANAGE_USERS"],
id: "my-saleor-app",
version: "1",
};
},
});
```

`<PLATFORM>` is one of the supported platforms, like NextJS or Lambda

Options provided to handler factory:

```typescript
type CreateManifestHandlerOptions = {
manifestFactory(context: {
appBaseUrl: string;
request: NextApiRequest;
schemaVersion: number | null;
}): AppManifest;
request: Request; // Depends on the platform, e.g. Request, NextApiRequest, NextRequest
schemaVersion: [major: number, minor: number] | null;
}): AppManifest; // Ensures response type is valid
};
```

You can use `NextApiRequest` to read additional parameters from the request.
You can use `request` to read additional parameters from the request.

Field `schemaVersion` can be used to enable some feature based on the Saleor version. It will be `null` if app is being installed in Saleor version below 3.15.0.
Field `schemaVersion` can be used to enable some feature based on the Saleor version. It will be `null` if request doesn't contain `saleor-schema-version` header.
Saleor will automatically attach this header, but the GET request executed e.g. from the browser will not contain this field.

See [`createManifestHandler`](https://github.com/saleor/saleor-app-sdk/blob/5c56cf566d2cc6e4a075c8c619f174fa43aad6c9/src/handlers/next/create-manifest-handler.ts#L18) for more details. See [manifest](https://github.com/saleor/saleor-app-sdk/blob/5c56cf566d2cc6e4a075c8c619f174fa43aad6c9/src/types.ts#L223) too.
Hint: `@saleor/app-sdk` contains documented types attached to the npm package.

### App register handler factory

Example usage of app register handler in Next.js
Following example shows how to use a register handler in Next.js:

```typescript
// pages/api/register.ts
// pages/api/register.ts - next.js route

import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
import { UpstashAPL } from "@saleor/app-sdk/APL";
import { UpstashAPL } from "@saleor/app-sdk/APL/upstash"; // See APL section

export default createAppRegisterHandler({
apl: new UpstashAPL({
Expand All @@ -79,6 +87,7 @@ export default createAppRegisterHandler({
return respondWithError({ message: "Error, installation will fail" });
});
},

});
```

Expand All @@ -97,7 +106,7 @@ export type CreateAppRegisterHandlerOptions = {
* Run right after Saleor calls this endpoint
*/
onRequestStart?(
request: Request,
request: Request, // Can be different depending on the platform
context: {
authToken?: string;
saleorDomain?: string;
Expand Down Expand Up @@ -149,3 +158,8 @@ See [APL](apl) for details on what is Auth Persistence Layer in Saleor apps.
App SDK provides a utility that helps build (async) webhook handlers so that the app can react to Saleor events.

Read about it [here](saleor-webhook).

### Protected handler

To protect endpoint from the outside world and accept only requests from the app's frontend, use the `createProtectedHandler` function.
See more details [here](./protected-handlers.mdx).
82 changes: 65 additions & 17 deletions docs/developer/extending/apps/developing-apps/app-sdk/apl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,47 @@ Auth Persistence Layer (APL) is a technology-agnostic interface for managing aut

The following doc contains a JavaScript / TypeScript implementation used by official Saleor apps.

## Available methods

- `get: (saleorApiUrl: string) => Promise<AuthData | undefined>` - If the entry for given saleorApiUrl exists, returns AuthData object.
The idea of APLs is to abstract specific persistent storage (like Redis, Upstash, or even a file) and provide a common interface for CRUD operations.
This way, you can easily switch between different storage solutions without changing the application code.
Additionally, app doesn't have to know any platform-specific details about the storage, like connection strings or endpoints.

- `set: (authData: AuthData) => Promise<void>` - Save auth data.
During development we recommend using `FileAPL` for fast and easy access on the local machine. For production you need to set up some form of database.

- `delete: (saleorApiUrl: string) => Promise<void>` - Remove auth data from the given API URL.
Hint: Specifics of APL is rarely write, but frequent read - it will be checked on every request to the app. We recommend to set up a database with fast read operations, like DynamoDB or Redis.

- `getAll: () => Promise<AuthData[]>` - Returns all auth data available.

- `isReady: () => Promise<AplReadyResult>` - Check if the persistence layer behind APL is ready. For example, when a database connection was established.

- `isConfigured: () => Promise<AplConfiguredResult>` - Check if the persistence layer behind APL is configured. For example, when an env variable is required by the database connection.
To increase the security, we recommend to store `token` encrypted.

## AuthData

Interface containing data used for communication with the Saleor API:

```ts
export interface AuthData {
domain: string;
// Token that Saleor provides to the app during installation. App must store it securely.
token: string;
// URL of Saleor GraphQL API, ending with /graphql/. Allows to identify the Saleor instance, especially in multi-tenant apps.
saleorApiUrl: string;
// ID of the app stored by Saleor in the database. It's unique for each installation. When app is reinstalled, ID will be different.
appId: string;
// Cached JWKS (JSON Web Key Set) used for webhook validation. It's available at https://<your-saleor-domain>/.well-known/jwks.json
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
## 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 from the given API URL.

- `getAll: () => Promise<AuthData[]>` - Returns all auth data available.

- `isReady?: () => Promise<AplReadyResult>` - Optional: Check if the persistence layer behind APL is ready. For example, when a database connection was established.

- `isConfigured?: () => Promise<AplConfiguredResult>` - Optional: Check if the persistence layer behind APL is configured. For example, when an env variable is required by the database connection.


## AplReadyResult & AplConfiguredResult

Expand All @@ -62,6 +70,8 @@ type AplConfiguredResult =
};
```

Implementing these functions is optional, but it can be useful for handling asynchronous operations like database connection.

## Example implementation

Let's create an APL which uses Redis for data storage:
Expand Down Expand Up @@ -122,6 +132,12 @@ Or access it from the context of API helpers from the SDK:
- [Protected API Handlers](./protected-handlers)
- [Webhook Handlers](./saleor-webhook)

Hint: You don't need to write RedisAPL on your own, you can import it from the SDK:

```ts
import { RedisAPL } from "@saleor/app-sdk/APL/redis";
```

### 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, you may like to use `FileAPL` during local development because it does not require any additional infrastructure. Deployed apps, on the other hand, need a more robust solution.
Expand All @@ -131,7 +147,8 @@ To handle both scenarios, initialize the proper APLs in your code based on its e
```ts
// lib/saleorApp.ts

import { FileAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { FileAPL } from "@saleor/app-sdk/APL/file";
import { UpstashAPL } from "@saleor/app-sdk/APL/upstash";

// Based on the environment variable, the app will use a different APL:
// - For local development store auth data in the `.auth-data.json`.
Expand Down Expand Up @@ -296,3 +313,34 @@ KV_REST_API_READ_ONLY_TOKEN=
# A key to the Redis collection. All APL items will be stored inside this collection
KV_STORAGE_NAMESPACE=
```

### RedisAPL

RedisAPL requires `redis` client to be installed.

Similar to VercelKV (which is Redis too), all data is stored in the hash collection. You can provide a custom key for the collection in the constructor

```typescript
import { createClient } from 'redis';
import { RedisAPL } from '@saleor/app-sdk/APL/redis';

const apl = new RedisAPL({
client: createClient(),
hashCollectionKey: 'my-key', // optional, by default "saleor_app_auth"
});

```

See Redis documentation for more details on how to set up the client.


### SaleorCloudAPL

You may see that there is a `SaleorCloudAPL` exported as well. This APL is specific implementation that Saleor Cloud uses when hosting apps on the cloud platform.

It's not available to use and will not be bundled by your app if not imported.

## Community APL implementations

- [Deno KV APL](https://github.com/witoszekdev/saleor-app-hono-deno-template/blob/main/server/deno-kv-apl.ts)
- [Cloudlfare KV APL](https://github.com/witoszekdev/saleor-app-hono-cf-pages-template/blob/main/src/cloudflare-kv-apl.ts)
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,21 @@ Available state represents `AppBridgeState`:

```typescript
type AppBridgeState = {
/**
* JWT token provided by Dashboard. Represents user's session.
*/
token?: string;
/**
* ID of the app
*/
id: string;
/**
* Flag if app bridge has properly initialized and authorized
*/
ready: boolean;
domain: string;
/**
* Current path on the frontend
*/
path: string;
theme: ThemeType;
locale: LocaleCode; // See src/locales.ts
Expand Down
Loading