From 21304aef919f567c6f5d57e477606f006824ff94 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 15 Jul 2024 11:32:49 +0200 Subject: [PATCH 01/13] wip --- .../extending/apps/building-payment-app.mdx | 132 ++++++++++++++++++ package-lock.json | 2 +- sidebars/building-apps.js | 1 + 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 docs/developer/extending/apps/building-payment-app.mdx diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx new file mode 100644 index 000000000..289927306 --- /dev/null +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -0,0 +1,132 @@ +--- +title: How to Build a Payment App +--- + +## Overview + +In this tutorial, you will learn how to create a basic Payment App with [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). + +:::note + +We will be integrating with a fictional **Dummy Payment Gateway**. The behavior of Dummy Payment Gateway will mimic a real payment gateway, but it will not actually process any payments. + +::: + +### Prerequisites + +- Basic understanding of [Saleor Apps](developer/extending/apps/overview.mdx) +- `node >=18.17.0 <=20` +- `pnpm >=9` + +### What is a Payment App + +Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a common interface for integrating with payment providers. Given that each payment provider comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). + +In this tutorial, we won't be covering all the API methods, but rather focus on the bare minimum required to build a Dummy Payment App. Keep in mind that a flow for integrating a real payment provider might be more complex. + +:::info + +If you need a reference point for integrating with an actual payment provider, you can check out the following examples: + +- [Example Stripe App](https://github.com/saleor/saleor-app-payment-stripe) +- [Example Authorize.net App](https://github.com/saleor/saleor-app-payment-authorize.net) +- [Example Klarna App](https://github.com/saleor/saleor-app-payment-klarna) + +::: + +The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview). This feature allows Saleor to suspend the resolution of an API call until a response from the app is received in the correct format. Saleor can then pass that response as a result of the API call. We will be looking at what webhooks we need to implement in [the next section](#webhooks). + +### What is a Transaction + +A transaction is an attempt to pay for an order. It is represented by a list of payment events. These events have their `status` field which expresses different stages of the payment process. + +### Dummy Payment Gateway Flow + +Payment Apps are meant to produce transactions. There are several webhooks that result in the creation of the transaction. The choice of the webhook depends on the payment flow that the payment provider implements. + +For this tutorial, we will assume that the Dummy Payment Gateway implements the following flow: + +1. Initiate the payment process (e.g., proceed to the payment page). +2. Ask for payment confirmation (e.g., redirect the user to the 3D Secure page). +3. Charge the payment on the 3D Secure page. +4. Redirect the user to the result (success or failure) page. + +## Modeling the Transaction Flow + +### Transaction Events + +:::tip + +Before starting any Payment App implementation, think of the transaction events you want to capture and how the app should transition between them. This will help you to choose the right webhooks and mutations. + +::: + +The first step in building our Payment App does not involve any code. To avoid further rework, we need to understand the transaction flow that we want to model. The end goal is to have a list of events produced by the app and their corresponding webhooks. + +Let's extract two important details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): + +- We want to successfully charge the payment. +- We can't charge the payment without the user's confirmation (3D Secure). + +That means the app can't go straight to the `CHARGE_SUCCESS` / `CHARGE_FAILURE` event status. Before that, the user needs to confirm the payment. This step will be represented by the `CHARGE_ACTION_REQUIRED` event status. + +Now, we can assign Saleor transaction event statuses to steps of the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): + +| Step | Status | Description | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| 1. Initiate the payment process | --- | --- | +| 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. | +| 3. Charge the payment | --- | --- | +| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_success) | The payment was successful. | +| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | +| 4. Redirect the user to the result page | --- | --- | + +### Webhooks + +Since we know what transaction events we want to capture, we can now choose the right webhooks to implement. + +When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that there are two webhooks that can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-initialize-session). + +There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_REQUESTED` webhook: + +1. It is designed for the staff users to manually charge the payment. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. +2. It does not allow to return the `CHARGE_ACTION_REQUIRED` event status. + +That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitializeSession`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can emit the `CHARGE_ACTION_REQUIRED` event status, as required. + +When `TRANSACTION_INITIALIZE_SESSION` returns the `CHARGE_ACTION_REQUIRED`, the app needs to have a way of processing the additional action. Based on the result of that operation, we will either transition to the `CHARGE_SUCCESS` or `CHARGE_FAILURE` event status. This is where the second webhook is needed: [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session). + +:::warning + +The `TRANSACTION_PROCESS_SESSION` webhook will only be called if `TRANSACTION_INITIALIZE_SESSION` returns the `CHARGE_ACTION_REQUIRED` event status. + +::: + +Here is the list of webhooks and their corresponding transaction event statuses: + +- `TRANSACTION_INITIALIZE_SESSION` -> `CHARGE_ACTION_REQUIRED` +- `TRANSACTION_PROCESS_SESSION` -> `CHARGE_SUCCESS` / `CHARGE_FAILURE` + +Now that we know the webhooks we will be implementing, we can finally start coding. + +## Creating a Saleor App from Template + +Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't be checking all the boxes manually. + +[`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the next sections of the tutorial. For now, let's clone the template and boot up our app: + +```bash +git clone https://github.com/saleor/saleor-app-template.git +``` + +Then, navigate to the app directory and install the dependencies: + +```bash +pnpm install +``` + +Finally, start the app: + +```bash +pnpm dev +``` diff --git a/package-lock.json b/package-lock.json index b39fd41a8..4dd404f2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "saleor-docs-2", + "name": "saleor-docs", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/sidebars/building-apps.js b/sidebars/building-apps.js index 06c4b2c74..191afbcc8 100644 --- a/sidebars/building-apps.js +++ b/sidebars/building-apps.js @@ -44,6 +44,7 @@ export const buildingApps = [ "developer/extending/apps/updating-app-webhooks", "developer/extending/apps/developing-apps/apps-patterns/handling-external-webhooks", "developer/extending/apps/developing-apps/apps-patterns/persistence-with-metadata-manager", + "developer/extending/apps/building-payment-app", // { // type: "category", From 71e1816c900f352375c3b2b870d28e23ed4ecab7 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 15 Jul 2024 13:59:47 +0200 Subject: [PATCH 02/13] wip --- .../extending/apps/building-payment-app.mdx | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 289927306..6b28d6f67 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -130,3 +130,163 @@ Finally, start the app: ```bash pnpm dev ``` + +## Implementing the First Webhook + +### App Manifest + +Our first step will be visiting the `src/pages/api/manifest.ts` directory, where the App Manifest lives. + +:::info + +[App Manifest](developer/extending/apps/architecture/manifest.mdx) is the source of truth for all the app-related metadata, including its webhooks. The `/manifest` API route is called by Saleor during the app installation to provide all the necessary information about the app. + +::: + +The `webhooks` array of the App Manifest should be filled with the webhook manifests: + +```ts +import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; +import { AppManifest } from "@saleor/app-sdk/types"; + +export default createManifestHandler({ + async manifestFactory({ appBaseUrl, request }) { + // ... + + const manifest: AppManifest = { + name: "Dummy Payment App", + // ... + webhooks: [], // 👈 provide webhook manifests here + // ... + }; + }, +}); +``` + +Luckily, we don't need to do that manually. + +[Saleor App SDK](developer/extending/apps/developing-apps/app-sdk/overview.mdx), a package included in the template, already provides helper classes for generating the webhooks: `SaleorSyncWebhook` and `SaleorAsyncWebhook`. These webhook instances have the `getWebhookManifest` method, which we can use to generate the webhook manifest. + +### Declaring `SaleorSyncWebhook` Instance + +In this step, we will be implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. + +Let's start by creating a new file in the `src/pages/api/webhooks` directory called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): + +```ts +import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; + +export const transactionInitializeSessionWebhook = + new SaleorSyncWebhook({ + name: "Transaction Initialize Session", + webhookPath: "api/webhooks/transaction-initialize-session", + event: "TRANSACTION_INITIALIZE_SESSION", + apl: saleorApp.apl, + query: "", // TODO: Add the subscription query + }); +``` + +Let's go through all the constructor parameters of the `SaleorSyncWebhook` class: + +- `name` - The name of the webhook. It will be used during the webhook registration process. +- `webhookPath` - The path to the webhook. It will be used to create the webhook URL. + `event` - The [synchronous webhook event](api-reference/webhooks/enums/webhook-event-type-sync-enum.mdx#values) that the handler will be listening to. +- `apl` - The reference to the app's [APL](developer/extending/apps/developing-apps/app-sdk/apl). Saleor App Template exports it from `src/saleor-app.ts`. +- `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we don't have our subscription query yet. +- `unknown` generic attribute - The type of the payload that the webhook will receive. Since we don't have the subscription query yet, we don't know what the payload will look like. + +### Building the Subscription Query + +With Subscription Webhook Payload, you can define the shape of the payload that the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK makes sure that the payload is correctly typed. + +Let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: + +```ts +import { gql } from "urql"; + +// 💡 We suggest keeping the payload in a fragment. It is easier this way to retrieve individual fields from the subscription. +const TransactionInitializeSessionPayload = gql` + fragment TransactionInitializeSessionPayload on TransactionInitializeSession { + action { + amount + currency + actionType + } + } +`; + +const TransactionInitializeSessionSubscription = gql` + # Payload fragment must be included in the root query + ${TransactionInitializeSessionPayload} + subscription TransactionInitializeSession { + event { + ...TransactionInitializeSessionPayload + } + } +`; +``` + +:::warning + +Remember that adding a new field to the subscription query requires re-registering the webhook in Saleor API. You can read more about this behavior in [How to Update App Webhooks](developer/extending/apps/updating-app-webhooks.mdx). + +::: + +Then, let's run the following command to generate the types: + +```bash +pnpm generate +``` + +When the command finishes, you should be able to import the type for the declared subscription query. With those two pieces, you can now update the `SaleorSyncWebhook` instance: + +```ts +import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; +import { TransactionInitializeSessionPayloadFragment } from "../../../../generated/graphql"; + +export const transactionInitializeSessionWebhook = + new SaleorSyncWebhook( + { + name: "Transaction Initialize Session", + webhookPath: "api/webhooks/transaction-initialize-session", + event: "TRANSACTION_INITIALIZE_SESSION", + apl: saleorApp.apl, + query: TransactionInitializeSessionSubscription, // 💾 Updated with the subscription query + } + ); +``` + +### Creating the Webhook Handler + +The last step is to create the webhook handler. The handler is a function that will be called when the webhook is triggered. `saleor-app-sdk` handler is a decorated Next.js API ([pages](https://nextjs.org/docs/pages)) route handler. The extra bit is the Saleor context that is passed to the handler. + +Let's extend our `transaction-initialize-session.ts` file with the handler boilerplate: + +```ts +export default transactionInitializeSessionWebhook.createHandler( + (req, res, ctx) => { + const { payload, event, baseUrl, authData } = ctx; + + // TODO: Implement the handler + + return res.status(200).end(); + } +); +``` + +The `ctx` object contains the following properties: + +- `payload` - The type-safe payload received from the webhook. +- `event` - Name of the event that triggered the webhook. +- `baseUrl` - The base URL of the app. If you need to register the app or webhook in an externak service, you can use this URL. +- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl#authdata) passed from Saleor to the app. Among other things, it contains the `token` that can be used to query Saleor API. + +## Testing the Webhook + +TODO + +## Implementing Transaction Process Session + +TODO From a0d3da2040695b494323c6263948507f70d36490 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 15 Jul 2024 14:50:34 +0200 Subject: [PATCH 03/13] wip --- .../extending/apps/building-payment-app.mdx | 107 ++++++++++++++++-- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 6b28d6f67..2876ae04e 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -92,7 +92,7 @@ There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_ 1. It is designed for the staff users to manually charge the payment. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. 2. It does not allow to return the `CHARGE_ACTION_REQUIRED` event status. -That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitializeSession`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can emit the `CHARGE_ACTION_REQUIRED` event status, as required. +That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can emit the `CHARGE_ACTION_REQUIRED` event status, as required. When `TRANSACTION_INITIALIZE_SESSION` returns the `CHARGE_ACTION_REQUIRED`, the app needs to have a way of processing the additional action. Based on the result of that operation, we will either transition to the `CHARGE_SUCCESS` or `CHARGE_FAILURE` event status. This is where the second webhook is needed: [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session). @@ -165,13 +165,13 @@ export default createManifestHandler({ Luckily, we don't need to do that manually. -[Saleor App SDK](developer/extending/apps/developing-apps/app-sdk/overview.mdx), a package included in the template, already provides helper classes for generating the webhooks: `SaleorSyncWebhook` and `SaleorAsyncWebhook`. These webhook instances have the `getWebhookManifest` method, which we can use to generate the webhook manifest. +[Saleor App SDK](developer/extending/apps/developing-apps/app-sdk/overview.mdx), a package included in the template, already provides helper classes for generating the webhooks: `SaleorSyncWebhook` and `SaleorAsyncWebhook`. Webhook instances created from these classes have the `getWebhookManifest` method, which we can use to generate the webhook manifest. ### Declaring `SaleorSyncWebhook` Instance -In this step, we will be implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. +In this step, we begin the process of implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. -Let's start by creating a new file in the `src/pages/api/webhooks` directory called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): +The first thing we need is a new file in the `src/pages/api/webhooks` directory, called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): ```ts import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; @@ -198,7 +198,9 @@ Let's go through all the constructor parameters of the `SaleorSyncWebhook` class ### Building the Subscription Query -With Subscription Webhook Payload, you can define the shape of the payload that the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK makes sure that the payload is correctly typed. +The next step will be filling that empty `query` attribute with an actual subscription query. + +With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload that the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK makes sure that the payload is correctly typed. Let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: @@ -233,7 +235,7 @@ Remember that adding a new field to the subscription query requires re-registeri ::: -Then, let's run the following command to generate the types: +Then, let's regenerate the types (they were initially generated during the dependencies installation): ```bash pnpm generate @@ -283,10 +285,97 @@ The `ctx` object contains the following properties: - `baseUrl` - The base URL of the app. If you need to register the app or webhook in an externak service, you can use this URL. - `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl#authdata) passed from Saleor to the app. Among other things, it contains the `token` that can be used to query Saleor API. +Since we are implementing the Dummy Payment Gateway, our handler doesn't need to call any external APIs or perform any complex operations. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. However, we need to remember that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): + +```ts +export default transactionInitializeSessionWebhook.createHandler( + (req, res, ctx) => { + const { payload, event, baseUrl, authData } = ctx; + + console.log("Transaction Initialize Session payload:", payload); + + const randomPspReference = crypto.randomUUID(); // Generate a random PSP reference + + return res.status(200).json({ + result: "CHARGE_ACTION_REQUIRED", + amount: payload.action.amount, + pspReference: randomPspReference, + // TODO: CHECK IF OTHER FIELDS ARE NEEDED + }); + } +); +``` + +Note that we are generating a random PSP reference. In a real-world scenario, this would be a transaction identifier returned by the payment provider. + +## Installing the App + +Now that we have implemented the first webhook, we can install the app in Saleor. To do that, we need to expose our local development environment to the internet via a tunneling service. You can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide to learn how to do that. + ## Testing the Webhook -TODO +Assuming that the app is installed in Saleor, we can now test the webhook. To do that, we need to trigger the `TRANSACTION_INITIALIZE_SESSION` event. We can do that by calling the `transactionInitialize` mutation from GraphQL Playground. Here is what the mutation looks like: -## Implementing Transaction Process Session +```graphql +mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { + transactionInitialize( + id: $checkoutId + paymentGateway: { id: $paymentGateway } + ) { + transaction { + id + } + transactionEvent { + id + type + } + errors { + field + message + code + } + } +} +``` + +The mutation requires two variables: `checkoutId` and `paymentGateway`. The `checkoutId` is the ID of the checkout that we want to pay for. The `paymentGateway` is the ID of the payment gateway that we want to use. + +We can see what payment gateways are available for the checkout by requesting the `availablePaymentGateways` field on the `checkout` query: + +```graphql +query GetCheckout($id: ID!) { + checkout(id: $id) { + availablePaymentGateways { + id + name + } + } +} +``` -TODO +If we installed the app correctly, we should see the `Dummy Payment Gateway` in the list of available payment gateways. The `id` will be drawn from the App Manifest's `id` field. + +If we have the `checkoutId` and `paymentGateway`, we can now call the `transactionInitialize` mutation. In the response, we should see the created transaction and the transaction event with the `CHARGE_ACTION_REQUIRED` type: + + + +```json +{ + "data": { + "transactionInitialize": { + "transaction": { + "id": "UHJvamVjdFRy" + }, + "transactionEvent": { + "id": "UHJvamVjdFRy", + "type": "CHARGE_ACTION_REQUIRED" + }, + "errors": [] + } + } +} +``` + +The app's console should also log the payload received from the webhook. + +## Implementing Transaction Process Session From e014a305cb3d5e38f684c92a77ac451783f5dfe6 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 16 Jul 2024 09:59:22 +0200 Subject: [PATCH 04/13] fix links --- docs/developer/extending/apps/building-payment-app.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 2876ae04e..f499381ab 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -34,12 +34,14 @@ If you need a reference point for integrating with an actual payment provider, y ::: -The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview). This feature allows Saleor to suspend the resolution of an API call until a response from the app is received in the correct format. Saleor can then pass that response as a result of the API call. We will be looking at what webhooks we need to implement in [the next section](#webhooks). +The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until a response from the app is received in the correct format. Saleor can then pass that response as a result of the API call. We will be looking at what webhooks we need to implement in [the next section](#webhooks). ### What is a Transaction A transaction is an attempt to pay for an order. It is represented by a list of payment events. These events have their `status` field which expresses different stages of the payment process. +TODO: finish + ### Dummy Payment Gateway Flow Payment Apps are meant to produce transactions. There are several webhooks that result in the creation of the transaction. The choice of the webhook depends on the payment flow that the payment provider implements. @@ -111,7 +113,7 @@ Now that we know the webhooks we will be implementing, we can finally start codi ## Creating a Saleor App from Template -Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't be checking all the boxes manually. +Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements.mdx) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't be checking all the boxes manually. [`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the next sections of the tutorial. For now, let's clone the template and boot up our app: @@ -192,7 +194,7 @@ Let's go through all the constructor parameters of the `SaleorSyncWebhook` class - `name` - The name of the webhook. It will be used during the webhook registration process. - `webhookPath` - The path to the webhook. It will be used to create the webhook URL. `event` - The [synchronous webhook event](api-reference/webhooks/enums/webhook-event-type-sync-enum.mdx#values) that the handler will be listening to. -- `apl` - The reference to the app's [APL](developer/extending/apps/developing-apps/app-sdk/apl). Saleor App Template exports it from `src/saleor-app.ts`. +- `apl` - The reference to the app's [APL](developer/extending/apps/developing-apps/app-sdk/apl.mdx). Saleor App Template exports it from `src/saleor-app.ts`. - `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we don't have our subscription query yet. - `unknown` generic attribute - The type of the payload that the webhook will receive. Since we don't have the subscription query yet, we don't know what the payload will look like. @@ -283,7 +285,7 @@ The `ctx` object contains the following properties: - `payload` - The type-safe payload received from the webhook. - `event` - Name of the event that triggered the webhook. - `baseUrl` - The base URL of the app. If you need to register the app or webhook in an externak service, you can use this URL. -- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl#authdata) passed from Saleor to the app. Among other things, it contains the `token` that can be used to query Saleor API. +- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` that can be used to query Saleor API. Since we are implementing the Dummy Payment Gateway, our handler doesn't need to call any external APIs or perform any complex operations. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. However, we need to remember that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): From e45a7222f5088e2577374bc7082927a45d326972 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 16 Jul 2024 14:29:36 +0200 Subject: [PATCH 05/13] wip --- .../extending/apps/building-payment-app.mdx | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index f499381ab..4aaf62992 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -8,7 +8,7 @@ In this tutorial, you will learn how to create a basic Payment App with [Transac :::note -We will be integrating with a fictional **Dummy Payment Gateway**. The behavior of Dummy Payment Gateway will mimic a real payment gateway, but it will not actually process any payments. +We will be integrating with a fictional **Dummy Payment Gateway**. The Dummy Payment Gateway will mimic a real payment gateway but not actually process any payments. ::: @@ -20,9 +20,9 @@ We will be integrating with a fictional **Dummy Payment Gateway**. The behavior ### What is a Payment App -Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a common interface for integrating with payment providers. Given that each payment provider comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). +Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a standard interface for integrating with payment providers. Given that each payment provider comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). -In this tutorial, we won't be covering all the API methods, but rather focus on the bare minimum required to build a Dummy Payment App. Keep in mind that a flow for integrating a real payment provider might be more complex. +In this tutorial, we won't cover the entire API surface but rather focus on the bare minimum required to build a Dummy Payment App. Remember that integrating a real payment provider might be more complex. :::info @@ -34,19 +34,17 @@ If you need a reference point for integrating with an actual payment provider, y ::: -The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until a response from the app is received in the correct format. Saleor can then pass that response as a result of the API call. We will be looking at what webhooks we need to implement in [the next section](#webhooks). +The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as a result of the API call. We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). ### What is a Transaction -A transaction is an attempt to pay for an order. It is represented by a list of payment events. These events have their `status` field which expresses different stages of the payment process. +A transaction is an attempt to pay for an order. It is represented by a list of payment events. These events have their `status` field expressing different payment process stages. TODO: finish ### Dummy Payment Gateway Flow -Payment Apps are meant to produce transactions. There are several webhooks that result in the creation of the transaction. The choice of the webhook depends on the payment flow that the payment provider implements. - -For this tutorial, we will assume that the Dummy Payment Gateway implements the following flow: +For this tutorial, we will assume that the Dummy Payment Gateway requires the following operations: 1. Initiate the payment process (e.g., proceed to the payment page). 2. Ask for payment confirmation (e.g., redirect the user to the 3D Secure page). @@ -59,18 +57,18 @@ For this tutorial, we will assume that the Dummy Payment Gateway implements the :::tip -Before starting any Payment App implementation, think of the transaction events you want to capture and how the app should transition between them. This will help you to choose the right webhooks and mutations. +Before starting any Payment App implementation, consider the transaction events you want to capture and how the app should transition between them. This will help you choose the right webhooks and mutations. ::: -The first step in building our Payment App does not involve any code. To avoid further rework, we need to understand the transaction flow that we want to model. The end goal is to have a list of events produced by the app and their corresponding webhooks. +The first step in building our Payment App does not involve any code. To avoid further rework, we need to understand the transaction flow we want to model. The goal is to have a list of events produced by the app and their corresponding webhooks. -Let's extract two important details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): +Let's extract two crucial details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): -- We want to successfully charge the payment. +- We want to charge the payment successfully. - We can't charge the payment without the user's confirmation (3D Secure). -That means the app can't go straight to the `CHARGE_SUCCESS` / `CHARGE_FAILURE` event status. Before that, the user needs to confirm the payment. This step will be represented by the `CHARGE_ACTION_REQUIRED` event status. +That means the app can't immediately return the `CHARGE_SUCCESS` / `CHARGE_FAILURE` event status. Before it does that, the user needs to confirm the payment. The `CHARGE_ACTION_REQUIRED` event status will represent this step. Now, we can assign Saleor transaction event statuses to steps of the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): @@ -87,35 +85,46 @@ Now, we can assign Saleor transaction event statuses to steps of the [Dummy Paym Since we know what transaction events we want to capture, we can now choose the right webhooks to implement. -When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that there are two webhooks that can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-initialize-session). +When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-initialize-session). There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_REQUESTED` webhook: -1. It is designed for the staff users to manually charge the payment. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. -2. It does not allow to return the `CHARGE_ACTION_REQUIRED` event status. +1. It is designed for the staff users to charge the payment manually. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. +2. It does not allow the return of the `CHARGE_ACTION_REQUIRED` event status. That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can emit the `CHARGE_ACTION_REQUIRED` event status, as required. -When `TRANSACTION_INITIALIZE_SESSION` returns the `CHARGE_ACTION_REQUIRED`, the app needs to have a way of processing the additional action. Based on the result of that operation, we will either transition to the `CHARGE_SUCCESS` or `CHARGE_FAILURE` event status. This is where the second webhook is needed: [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session). - -:::warning - -The `TRANSACTION_PROCESS_SESSION` webhook will only be called if `TRANSACTION_INITIALIZE_SESSION` returns the `CHARGE_ACTION_REQUIRED` event status. - -::: +When `TRANSACTION_INITIALIZE_SESSION` returns `CHARGE_ACTION_REQUIRED`, the app must execute this additional action. Depending on the outcome, we want the status to transition to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. The additional [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session) webhook will be responsible for this transition. Here is the list of webhooks and their corresponding transaction event statuses: - `TRANSACTION_INITIALIZE_SESSION` -> `CHARGE_ACTION_REQUIRED` - `TRANSACTION_PROCESS_SESSION` -> `CHARGE_SUCCESS` / `CHARGE_FAILURE` -Now that we know the webhooks we will be implementing, we can finally start coding. +And the sequence diagram of the transaction flow: + +```mermaid +sequenceDiagram + participant Storefront as Storefront + participant Saleor as Saleor + participant App as Dummy Payment App + Storefront->>Saleor: transactionInitialize + Saleor->>App: TRANSACTION_INITIALIZE_SESSION + App-->>Saleor: { result: CHARGE_ACTION_REQUIRED } + Saleor->>Storefront: { type: CHARGE_ACTION_REQUIRED, ... } + Storefront->>Saleor: transactionProcess + Saleor->>App: TRANSACTION_PROCESS_SESSION + App-->>Saleor: {result: CHARGE_SUCCESS / CHARGE_FAILURE } + Saleor->>Storefront: { type: CHARGE_SUCCESS / CHARGE_FAILURE, ... } +``` + +Now that we know the webhooks we will implement, we can finally start coding. ## Creating a Saleor App from Template -Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements.mdx) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't be checking all the boxes manually. +Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements.mdx) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't have to manually check all the boxes. -[`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the next sections of the tutorial. For now, let's clone the template and boot up our app: +[`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the following sections of the tutorial. For now, let's clone the template and boot up our app: ```bash git clone https://github.com/saleor/saleor-app-template.git @@ -141,7 +150,7 @@ Our first step will be visiting the `src/pages/api/manifest.ts` directory, where :::info -[App Manifest](developer/extending/apps/architecture/manifest.mdx) is the source of truth for all the app-related metadata, including its webhooks. The `/manifest` API route is called by Saleor during the app installation to provide all the necessary information about the app. +[App Manifest](developer/extending/apps/architecture/manifest.mdx) is the source of truth for all the app-related metadata, including its webhooks. Saleor calls the `/manifest` API route during the app installation to provide all the necessary information about the app. ::: @@ -154,10 +163,8 @@ import { AppManifest } from "@saleor/app-sdk/types"; export default createManifestHandler({ async manifestFactory({ appBaseUrl, request }) { // ... - const manifest: AppManifest = { name: "Dummy Payment App", - // ... webhooks: [], // 👈 provide webhook manifests here // ... }; @@ -171,9 +178,9 @@ Luckily, we don't need to do that manually. ### Declaring `SaleorSyncWebhook` Instance -In this step, we begin the process of implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. +In this step, we begin implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. -The first thing we need is a new file in the `src/pages/api/webhooks` directory, called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): +We first need a new file in the `src/pages/api/webhooks` directory, called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): ```ts import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; @@ -192,24 +199,24 @@ export const transactionInitializeSessionWebhook = Let's go through all the constructor parameters of the `SaleorSyncWebhook` class: - `name` - The name of the webhook. It will be used during the webhook registration process. -- `webhookPath` - The path to the webhook. It will be used to create the webhook URL. +- `webhookPath` - The path to the webhook. `event` - The [synchronous webhook event](api-reference/webhooks/enums/webhook-event-type-sync-enum.mdx#values) that the handler will be listening to. - `apl` - The reference to the app's [APL](developer/extending/apps/developing-apps/app-sdk/apl.mdx). Saleor App Template exports it from `src/saleor-app.ts`. -- `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we don't have our subscription query yet. -- `unknown` generic attribute - The type of the payload that the webhook will receive. Since we don't have the subscription query yet, we don't know what the payload will look like. +- `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we still need to declare our subscription query. +- `unknown` generic attribute - The type of webhook payload the app will receive. Since we don't have the subscription query yet, it is `unknown`. ### Building the Subscription Query The next step will be filling that empty `query` attribute with an actual subscription query. -With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload that the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK makes sure that the payload is correctly typed. +With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed. Let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: ```ts import { gql } from "urql"; -// 💡 We suggest keeping the payload in a fragment. It is easier this way to retrieve individual fields from the subscription. +// 💡 We suggest keeping the payload in a fragment. It is easier to retrieve individual fields from the subscription this way. const TransactionInitializeSessionPayload = gql` fragment TransactionInitializeSessionPayload on TransactionInitializeSession { action { @@ -237,7 +244,7 @@ Remember that adding a new field to the subscription query requires re-registeri ::: -Then, let's regenerate the types (they were initially generated during the dependencies installation): +Then, let's regenerate the types (the app initially generated them during the dependencies installation): ```bash pnpm generate @@ -284,10 +291,21 @@ The `ctx` object contains the following properties: - `payload` - The type-safe payload received from the webhook. - `event` - Name of the event that triggered the webhook. -- `baseUrl` - The base URL of the app. If you need to register the app or webhook in an externak service, you can use this URL. -- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` that can be used to query Saleor API. +- `baseUrl` - The base URL of the app. If you need to register the app or webhook in an external service, you can use this URL. +- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` you can use to query Saleor API. -Since we are implementing the Dummy Payment Gateway, our handler doesn't need to call any external APIs or perform any complex operations. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. However, we need to remember that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): +Since we are implementing the Dummy Payment Gateway, our handler will be basic. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. However, we need to remember that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): + +:::info + +Here is what a real-world handler could do: + +- Transform the payload into a format expected by the payment provider. +- Call the payment provider API (to, for example, create a payment intent). +- Process the response from the payment provider. +- Return the response from the webhook. + +::: ```ts export default transactionInitializeSessionWebhook.createHandler( @@ -312,11 +330,11 @@ Note that we are generating a random PSP reference. In a real-world scenario, th ## Installing the App -Now that we have implemented the first webhook, we can install the app in Saleor. To do that, we need to expose our local development environment to the internet via a tunneling service. You can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide to learn how to do that. +Now that we have implemented the first webhook, we can install the app in Saleor. We need to expose our local development environment to the internet via a tunneling service to do that. You can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide to learn how to do that. ## Testing the Webhook -Assuming that the app is installed in Saleor, we can now test the webhook. To do that, we need to trigger the `TRANSACTION_INITIALIZE_SESSION` event. We can do that by calling the `transactionInitialize` mutation from GraphQL Playground. Here is what the mutation looks like: +Assuming you installed the app in Saleor, we can test the webhook. We need to trigger the `TRANSACTION_INITIALIZE_SESSION` event by calling the `transactionInitialize` mutation from GraphQL Playground. Here is what the mutation looks like: ```graphql mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { @@ -381,3 +399,9 @@ If we have the `checkoutId` and `paymentGateway`, we can now call the `transacti The app's console should also log the payload received from the webhook. ## Implementing Transaction Process Session + +:::warning + +The `transactionProcess` mutation will only reach the app if the previous call to `TRANSACTION_INITIALIZE_SESSION` returned either `CHARGE_ACTION_REQUIRED` or `AUTHORIZE_ACTION_REQUIRED` event status. + +::: From 7628594b09d071a409ad699e19c6a12cf14651fa Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 16 Jul 2024 14:52:05 +0200 Subject: [PATCH 06/13] fix links successfully --- .../extending/apps/building-payment-app.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 4aaf62992..c7e4839eb 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -72,20 +72,20 @@ That means the app can't immediately return the `CHARGE_SUCCESS` / `CHARGE_FAILU Now, we can assign Saleor transaction event statuses to steps of the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): -| Step | Status | Description | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| 1. Initiate the payment process | --- | --- | -| 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. | -| 3. Charge the payment | --- | --- | -| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_success) | The payment was successful. | -| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | -| 4. Redirect the user to the result page | --- | --- | +| Step | Status | Description | +| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| 1. Initiate the payment process | --- | --- | +| 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. | +| 3. Charge the payment | --- | --- | +| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) | The payment was successful. | +| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | +| 4. Redirect the user to the result page | --- | --- | ### Webhooks Since we know what transaction events we want to capture, we can now choose the right webhooks to implement. -When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-initialize-session). +When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#request-4). There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_REQUESTED` webhook: From 792ad6c022d4411951f856d21054cd571f3eb8dc Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 18 Jul 2024 12:43:38 +0200 Subject: [PATCH 07/13] more --- .../extending/apps/building-payment-app.mdx | 311 +++++++++++++++--- 1 file changed, 270 insertions(+), 41 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index c7e4839eb..e27d8eb9e 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -20,13 +20,17 @@ We will be integrating with a fictional **Dummy Payment Gateway**. The Dummy Pay ### What is a Payment App -Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a standard interface for integrating with payment providers. Given that each payment provider comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). +Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a standard payment interface. It allows Saleor to recognize the app as a payment provider and delegate the payment process to it. -In this tutorial, we won't cover the entire API surface but rather focus on the bare minimum required to build a Dummy Payment App. Remember that integrating a real payment provider might be more complex. +That interface consists of [synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as a result of the API call. We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). + +Given that each payment service comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx), which we will be using for processing the payments. + +Our simple, fictional payment gateway won't make us cover the entire API surface. Keep in mind that in case of integrating with a real payment provider, you will need to implement more webhooks and mutations. :::info -If you need a reference point for integrating with an actual payment provider, you can check out the following examples: +If you need a reference point for integrating with real payment providers, you can check out the following examples: - [Example Stripe App](https://github.com/saleor/saleor-app-payment-stripe) - [Example Authorize.net App](https://github.com/saleor/saleor-app-payment-authorize.net) @@ -34,19 +38,19 @@ If you need a reference point for integrating with an actual payment provider, y ::: -The centrepiece of any Payment App are [the synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as a result of the API call. We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). - ### What is a Transaction -A transaction is an attempt to pay for an order. It is represented by a list of payment events. These events have their `status` field expressing different payment process stages. +A transaction represents a payment event that happened in Order or Checkout. These events include actions like charging a payment, authorizing or refunding it. You can see the full list of events in the [`TransactionEventTypeEnum`](api-reference/payments/enums/transaction-event-type-enum.mdx). -TODO: finish +Besides the events, a transaction also contains the payment information, like the amount and currency. Transactions can be created and managed using the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). ### Dummy Payment Gateway Flow -For this tutorial, we will assume that the Dummy Payment Gateway requires the following operations: +For this tutorial, we will assume that the Dummy Payment Gateway we will be integrating with implements the following flow: -1. Initiate the payment process (e.g., proceed to the payment page). +Dummy Payment Gateway offers a drop-in payment UI that returns a token when the user starts the payment. The token should be used to confirm the payment on the backend. + +1. Initiate the payment process in the drop-in. 2. Ask for payment confirmation (e.g., redirect the user to the 3D Secure page). 3. Charge the payment on the 3D Secure page. 4. Redirect the user to the result (success or failure) page. @@ -72,14 +76,14 @@ That means the app can't immediately return the `CHARGE_SUCCESS` / `CHARGE_FAILU Now, we can assign Saleor transaction event statuses to steps of the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): -| Step | Status | Description | -| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| 1. Initiate the payment process | --- | --- | -| 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. | -| 3. Charge the payment | --- | --- | -| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) | The payment was successful. | -| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | -| 4. Redirect the user to the result page | --- | --- | +| Step | Status | Description | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| 1. Initiate the payment process in the drop-in | --- | --- | +| 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. **The drop-in token is needed to initialize the payment.** | +| 3. Charge the payment | --- | --- | +| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) | The payment was successful. | +| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | +| 4. Redirect the user to the result page | --- | --- | ### Webhooks @@ -142,7 +146,11 @@ Finally, start the app: pnpm dev ``` -## Implementing the First Webhook +## Installing the App + +To verify that the app is running correctly, we must install it in Saleor. To do that, we need to expose our local development environment to the internet via a tunneling service. If you don't have experience with tunneling, you can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide. + +## Implementing Transaction Initialize Session ### App Manifest @@ -205,13 +213,29 @@ Let's go through all the constructor parameters of the `SaleorSyncWebhook` class - `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we still need to declare our subscription query. - `unknown` generic attribute - The type of webhook payload the app will receive. Since we don't have the subscription query yet, it is `unknown`. -### Building the Subscription Query +### Defining the Subscription Query The next step will be filling that empty `query` attribute with an actual subscription query. -With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed. +With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed.] + +#### The `data` Field + +Besides our generic payment-related fields, there is one field we are especially interested in: the `data` field. + +There are no requirements for the content of the `data` field, as long as it is a valid JSON object. We can use it to pass any custom information that the payment provider requires. -Let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: +In our case, we will use it to pass the token received from the drop-in. In the ["Calling the `transactionInitialize` Mutation"](#calling-the-transactioninitialize-mutation) section, we will provide the token as the mutation variable. In the app, we have to extract it from the payload. + +#### The Subscription Query + +With that knowledge, let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: + +:::info + +In Saleor App Template, we use [urql](https://formidable.com/open-source/urql/) to write GraphQL queries. If you prefer a different client, you still should be able to follow along. + +::: ```ts import { gql } from "urql"; @@ -238,12 +262,6 @@ const TransactionInitializeSessionSubscription = gql` `; ``` -:::warning - -Remember that adding a new field to the subscription query requires re-registering the webhook in Saleor API. You can read more about this behavior in [How to Update App Webhooks](developer/extending/apps/updating-app-webhooks.mdx). - -::: - Then, let's regenerate the types (the app initially generated them during the dependencies installation): ```bash @@ -269,6 +287,34 @@ export const transactionInitializeSessionWebhook = ); ``` +Now that we have the webhook instance, we can use it to populate the `webhooks` array in the App Manifest: + +```ts +// src/pages/api/manifest.ts +import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; +import { AppManifest } from "@saleor/app-sdk/types"; +import { transactionInitializeSessionWebhook } from "./webhooks/transaction-initialize-session"; + +export default createManifestHandler({ + async manifestFactory({ appBaseUrl }) { + // ... + const manifest: AppManifest = { + name: "Dummy Payment App", + webhooks: [ + transactionInitializeSessionWebhook.getWebhookManifest(appBaseUrl), + ], + // ... + }; + }, +}); +``` + +:::warning + +Remember that modifying an existing subscription query or adding a new webhook requires manual webhook update in Saleor API. The easiest way to do it is to reinstall the app. You can read more about this behavior in [How to Update App Webhooks](developer/extending/apps/updating-app-webhooks.mdx). + +::: + ### Creating the Webhook Handler The last step is to create the webhook handler. The handler is a function that will be called when the webhook is triggered. `saleor-app-sdk` handler is a decorated Next.js API ([pages](https://nextjs.org/docs/pages)) route handler. The extra bit is the Saleor context that is passed to the handler. @@ -294,16 +340,18 @@ The `ctx` object contains the following properties: - `baseUrl` - The base URL of the app. If you need to register the app or webhook in an external service, you can use this URL. - `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` you can use to query Saleor API. -Since we are implementing the Dummy Payment Gateway, our handler will be basic. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. However, we need to remember that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): +Since we are implementing the Dummy Payment Gateway, our handler logic will be basic. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. The only thing we need to remember is that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): + + :::info -Here is what a real-world handler could do: +Here is what a real-world `TRANSACTION_INITIALIZE_SESSION` webhook handler might do: +- Retrieve some kind of token from the `payload.data` to identify the payment. - Transform the payload into a format expected by the payment provider. - Call the payment provider API (to, for example, create a payment intent). -- Process the response from the payment provider. -- Return the response from the webhook. +- Process the response from the payment provider and return it. ::: @@ -318,7 +366,7 @@ export default transactionInitializeSessionWebhook.createHandler( return res.status(200).json({ result: "CHARGE_ACTION_REQUIRED", - amount: payload.action.amount, + amount: payload.action.amount, // `payload` is typed thanks to the generated types pspReference: randomPspReference, // TODO: CHECK IF OTHER FIELDS ARE NEEDED }); @@ -326,15 +374,15 @@ export default transactionInitializeSessionWebhook.createHandler( ); ``` -Note that we are generating a random PSP reference. In a real-world scenario, this would be a transaction identifier returned by the payment provider. +By returning the `CHARGE_ACTION_REQUIRED` event status, we are informing Saleor that the payment requires additional action to be completed. We will perform this action in the next webhook handler. -## Installing the App +Besides the `result` field, we also return the `amount` and `pspReference` fields. The `amount` is the amount of the transaction, and the `pspReference` is a unique identifier of the payment in the payment provider system. Since we are not integrating with a real payment provider, we generate a random `pspReference`. -Now that we have implemented the first webhook, we can install the app in Saleor. We need to expose our local development environment to the internet via a tunneling service to do that. You can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide to learn how to do that. +### Calling the `transactionInitialize` Mutation -## Testing the Webhook +Assuming you installed the app in Saleor, we can finally try out our webhook handler. We can trigger the `TRANSACTION_INITIALIZE_SESSION` event by calling the `transactionInitialize` mutation from [GraphQL Playground](docs/api-usage/developer-tools.mdx#playground). -Assuming you installed the app in Saleor, we can test the webhook. We need to trigger the `TRANSACTION_INITIALIZE_SESSION` event by calling the `transactionInitialize` mutation from GraphQL Playground. Here is what the mutation looks like: +Here is what the mutation looks like: ```graphql mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { @@ -358,9 +406,11 @@ mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { } ``` -The mutation requires two variables: `checkoutId` and `paymentGateway`. The `checkoutId` is the ID of the checkout that we want to pay for. The `paymentGateway` is the ID of the payment gateway that we want to use. +The mutation requires two variables: `checkoutId` and `paymentGateway`. + +The `checkoutId` is the ID of the checkout that we want to pay for. -We can see what payment gateways are available for the checkout by requesting the `availablePaymentGateways` field on the `checkout` query: +The `paymentGateway` is the ID of the payment gateway that we want to use. We can see what payment gateways are available for the checkout by requesting the `availablePaymentGateways` field on the `checkout` query: ```graphql query GetCheckout($id: ID!) { @@ -373,7 +423,7 @@ query GetCheckout($id: ID!) { } ``` -If we installed the app correctly, we should see the `Dummy Payment Gateway` in the list of available payment gateways. The `id` will be drawn from the App Manifest's `id` field. +If we installed the app correctly, we should see the "Dummy Payment Gateway" in the list of available payment gateways. The `id` will be drawn from the App Manifest's `id` field. If we have the `checkoutId` and `paymentGateway`, we can now call the `transactionInitialize` mutation. In the response, we should see the created transaction and the transaction event with the `CHARGE_ACTION_REQUIRED` type: @@ -387,7 +437,7 @@ If we have the `checkoutId` and `paymentGateway`, we can now call the `transacti "id": "UHJvamVjdFRy" }, "transactionEvent": { - "id": "UHJvamVjdFRy", + "id": "VHJhbnNhY3Rpb25FdmVudDoz", "type": "CHARGE_ACTION_REQUIRED" }, "errors": [] @@ -400,8 +450,187 @@ The app's console should also log the payload received from the webhook. ## Implementing Transaction Process Session +### Creating the Webhook Handler + :::warning The `transactionProcess` mutation will only reach the app if the previous call to `TRANSACTION_INITIALIZE_SESSION` returned either `CHARGE_ACTION_REQUIRED` or `AUTHORIZE_ACTION_REQUIRED` event status. ::: + +When successfully called, our first webhook handler returned the `CHARGE_ACTION_REQUIRED` event status. Now, we need to implement the second webhook handler, `TRANSACTION_PROCESS_SESSION`, to transition the status to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. + +We will start by repeating the steps from the previous section. Let's create a new file in the `src/pages/api/webhooks` directory, called `transaction-process-session.ts`. + +Then, declare the subscription query for the `TRANSACTION_PROCESS_SESSION` webhook: + +```ts +import { TransactionProcessSessionPayloadFragment } from "../../../../generated/graphql"; // Import the generated payload type +import { gql } from "urql"; + +// 💡 Remember to regenerate the types after adding the new subscription query +const TransactionProcessSessionPayload = gql` + fragment TransactionProcessSessionPayload on TransactionProcessSession { + action { + amount + currency + actionType + } + } +`; + +const TransactionProcessSessionSubscription = gql` + ${TransactionProcessSessionPayload} + subscription TransactionProcessSession { + event { + ...TransactionProcessSessionPayload + } + } +`; +``` + +And then, create the `SaleorSyncWebhook` instance, as well as the webhook handler: + +```ts +import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; +// ... + +export const transactionProcessSessionWebhook = + new SaleorSyncWebhook({ + name: "Transaction Process Session", + webhookPath: "api/webhooks/transaction-process-session", + event: "TRANSACTION_PROCESS_SESSION", + apl: saleorApp.apl, + query: TransactionProcessSessionSubscription, + }); + +export default transactionProcessSessionWebhook.createHandler( + (req, res, ctx) => { + const { payload, event, baseUrl, authData } = ctx; + + console.log("Transaction Process Session payload:", payload); + + return res.status(200).json({ + result: "CHARGE_SUCCESS", + amount: payload.action.amount, + pspReference: crypto.randomUUID(), + }); + } +); +``` + +:::info + +Here is what a real-world `TRANSACTION_PROCESS_SESSION` webhook handler might do: + +- Call the payment provider API to check the payment status. +- Process the response from the payment provider and return the `CHARGE_SUCCESS` or `CHARGE_FAILURE` event status. + +::: + +If the handler would perform a logic that can fail, we should try to catch the error and return the `CHARGE_FAILURE` event status: + +```ts +export default transactionProcessSessionWebhook.createHandler( + (req, res, ctx) => { + const { payload, event, baseUrl, authData } = ctx; + + console.log("Transaction Process Session payload:", payload); + + try { + doSomethingThatCanFail(); // This function can throw an error + + return res.status(200).json({ + result: "CHARGE_SUCCESS", + amount: payload.action.amount, + pspReference: crypto.randomUUID(), + }); + } catch (error) { + return res.status(200).json({ + result: "CHARGE_FAILURE", + amount: payload.action.amount, + pspReference: crypto.randomUUID(), + }); + } + } +); +``` + +The last step is to update the App Manifest with the new webhook: + +```ts +// src/pages/api/manifest.ts +import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; +import { AppManifest } from "@saleor/app-sdk/types"; +import { transactionInitializeSessionWebhook } from "./webhooks/transaction-initialize-session"; +// highlight-next-line +import { transactionProcessSessionWebhook } from "./webhooks/transaction-process-session"; + +export default createManifestHandler({ + async manifestFactory({ appBaseUrl }) { + // ... + const manifest: AppManifest = { + name: "Dummy Payment App", + webhooks: [ + transactionInitializeSessionWebhook.getWebhookManifest(appBaseUrl), + // highlight-next-line + transactionProcessSessionWebhook.getWebhookManifest(appBaseUrl), + ], + // ... + }; + }, +}); +``` + +Once again, remember to update the app's webhooks or reinstall the app in Saleor. Otherwise, the new webhook handler won't be called. + +### Calling the `transactionProcess` Mutation + +To test the `TRANSACTION_PROCESS_SESSION` webhook, we need to call the `transactionProcess` mutation. The mutation requires the `transactionId` variable, which is the ID of the transaction we want to process. That will be the `id` field from the `transaction` object returned by the `transactionInitialize` mutation. + +Here is what the mutation looks like: + +```graphql +mutation TransactionProcess($transactionId: ID!) { + transactionProcess(id: $transactionId) { + transaction { + id + } + transactionEvent { + id + type + } + errors { + field + message + code + } + } +} +``` + +In response, we should receive the processed transaction and the corresponding transaction event with the `CHARGE_SUCCESS` type: + +```json +{ + "data": { + "transactionProcess": { + "transaction": { + "id": "UHJvamVjdFRy" + }, + "transactionEvent": { + "id": "VHJhbnNhY3Rpb25FdmVudDoz", + "type": "CHARGE_SUCCESS" + }, + "errors": [] + } + } +} +``` + +## Next steps + +- Payment gateway initialize +- Storing payment methods +- Service → Saleor `transactionEventReport` webhook From 0f5d1112e05f90c39c2ad59b760410851aa10f11 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 19 Jul 2024 08:36:34 +0200 Subject: [PATCH 08/13] finish the content --- docs/developer/extending/apps/building-payment-app.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index e27d8eb9e..973097d3d 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -629,8 +629,12 @@ In response, we should receive the processed transaction and the corresponding t } ``` +And that concludes the implementation of the Dummy Payment Gateway. With just two webhooks, we managed to model a 3D Secure payment flow. + ## Next steps -- Payment gateway initialize -- Storing payment methods -- Service → Saleor `transactionEventReport` webhook +Once you have covered the basics, you can extend the app with more features. Here are some ideas: + +- If your payment provider requires additional operation even before the payment is initialized (for example, to render the drop-in), you might be interested in reading about [`PAYMENT_GATEWAY_INITIALIZE_SESSION`](developer/payments.mdx#initialize-payment-gateway). +- If you want to allow users to save their payment methods for future use, you should implement the [Stored Payment Methods API](developer/payments.mdx#stored-payment-methods). +- If your payment provider needs to report the transaction status to Saleor asynchronously, you can use the [`transactionEventReport` mutation](developer/payments.mdx#reporting-actions-for-transactions). From c1d4c223d51d0343e2aa10d4915b8355aa6ac4f4 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 19 Jul 2024 10:52:40 +0200 Subject: [PATCH 09/13] proofreading --- .../extending/apps/building-payment-app.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 973097d3d..e5fe25303 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -4,11 +4,11 @@ title: How to Build a Payment App ## Overview -In this tutorial, you will learn how to create a basic Payment App with [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). +This tutorial will guide you through creating a basic Payment App with [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). :::note -We will be integrating with a fictional **Dummy Payment Gateway**. The Dummy Payment Gateway will mimic a real payment gateway but not actually process any payments. +We will integrate with a fictional **Dummy Payment Gateway**. This gateway will simulate real payment processing without handling actual payments. ::: @@ -20,13 +20,13 @@ We will be integrating with a fictional **Dummy Payment Gateway**. The Dummy Pay ### What is a Payment App -Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that implements a standard payment interface. It allows Saleor to recognize the app as a payment provider and delegate the payment process to it. +A Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that follows a standard payment interface, enabling Saleor to identify the app as a payment gateway and manage payments through it. That interface consists of [synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as a result of the API call. We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). -Given that each payment service comes with its unique payment flow, Saleor must provide an API flexible enough to accommodate all of them. That was the key principle behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx), which we will be using for processing the payments. +Each payment service has its unique payment flow, so Saleor needs a flexible API to support them. This flexibility is the core idea behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx), which we will use for processing payments. -Our simple, fictional payment gateway won't make us cover the entire API surface. Keep in mind that in case of integrating with a real payment provider, you will need to implement more webhooks and mutations. +Our fictional payment gateway simplifies the implementation, so we won't cover the entire API. For real payment providers, you'll need to implement additional webhooks and mutations. :::info @@ -67,7 +67,7 @@ Before starting any Payment App implementation, consider the transaction events The first step in building our Payment App does not involve any code. To avoid further rework, we need to understand the transaction flow we want to model. The goal is to have a list of events produced by the app and their corresponding webhooks. -Let's extract two crucial details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): +Let's highlight two key details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): - We want to charge the payment successfully. - We can't charge the payment without the user's confirmation (3D Secure). @@ -87,7 +87,7 @@ Now, we can assign Saleor transaction event statuses to steps of the [Dummy Paym ### Webhooks -Since we know what transaction events we want to capture, we can now choose the right webhooks to implement. +Now that we've identified the transaction events, we can select the appropriate webhooks to implement. When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#request-4). @@ -215,7 +215,7 @@ Let's go through all the constructor parameters of the `SaleorSyncWebhook` class ### Defining the Subscription Query -The next step will be filling that empty `query` attribute with an actual subscription query. +Next, we'll fill in the empty `query` attribute with an actual subscription query. With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed.] @@ -458,7 +458,7 @@ The `transactionProcess` mutation will only reach the app if the previous call t ::: -When successfully called, our first webhook handler returned the `CHARGE_ACTION_REQUIRED` event status. Now, we need to implement the second webhook handler, `TRANSACTION_PROCESS_SESSION`, to transition the status to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. +After our first webhook handler returns the `CHARGE_ACTION_REQUIRED` status, we need to implement the `TRANSACTION_PROCESS_SESSION` webhook handler to update the status to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. We will start by repeating the steps from the previous section. Let's create a new file in the `src/pages/api/webhooks` directory, called `transaction-process-session.ts`. From f3bf53d59029400d49165d87cc3e9839aa8b05e9 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 19 Jul 2024 14:00:27 +0200 Subject: [PATCH 10/13] further proofreading --- .../extending/apps/building-payment-app.mdx | 434 +++++++++++++----- 1 file changed, 325 insertions(+), 109 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index e5fe25303..281b7ecf9 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -15,6 +15,7 @@ We will integrate with a fictional **Dummy Payment Gateway**. This gateway will ### Prerequisites - Basic understanding of [Saleor Apps](developer/extending/apps/overview.mdx) +- Familiarity with [Next.js](https://nextjs.org/) - `node >=18.17.0 <=20` - `pnpm >=9` @@ -22,11 +23,13 @@ We will integrate with a fictional **Dummy Payment Gateway**. This gateway will A Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that follows a standard payment interface, enabling Saleor to identify the app as a payment gateway and manage payments through it. -That interface consists of [synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as a result of the API call. We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). +That interface consists of [synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as the value of the requested field. + +We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). Each payment service has its unique payment flow, so Saleor needs a flexible API to support them. This flexibility is the core idea behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx), which we will use for processing payments. -Our fictional payment gateway simplifies the implementation, so we won't cover the entire API. For real payment providers, you'll need to implement additional webhooks and mutations. +Our fictional payment gateway is a simple one, so we won't cover the entire API in this tutorial. For real payment providers, you may need to implement additional webhooks and mutations. :::info @@ -40,54 +43,68 @@ If you need a reference point for integrating with real payment providers, you c ### What is a Transaction -A transaction represents a payment event that happened in Order or Checkout. These events include actions like charging a payment, authorizing or refunding it. You can see the full list of events in the [`TransactionEventTypeEnum`](api-reference/payments/enums/transaction-event-type-enum.mdx). +A transaction represents a payment event that happened in Order or Checkout. These events include actions like charging a payment, authorizing or refunding it. You can see the complete list of events in the [`TransactionEventTypeEnum`](api-reference/payments/enums/transaction-event-type-enum.mdx). -Besides the events, a transaction also contains the payment information, like the amount and currency. Transactions can be created and managed using the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). +Besides the events, a transaction also contains other payment information, like the amount or currency. -### Dummy Payment Gateway Flow +Transactions can be created and managed using the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx). -For this tutorial, we will assume that the Dummy Payment Gateway we will be integrating with implements the following flow: +### Dummy Payment Gateway Flow -Dummy Payment Gateway offers a drop-in payment UI that returns a token when the user starts the payment. The token should be used to confirm the payment on the backend. +We want Dummy Payment Gateway to feel like a real payment provider. To achieve that, we will model the payment process after a typical 3D Secure flow. -1. Initiate the payment process in the drop-in. -2. Ask for payment confirmation (e.g., redirect the user to the 3D Secure page). -3. Charge the payment on the 3D Secure page. -4. Redirect the user to the result (success or failure) page. +Here is how it looks: -## Modeling the Transaction Flow +> Dummy Payment Gateway offers a drop-in payment UI that returns a token when the user starts the payment. The token should be used to confirm the payment on the backend. +> +> The payment process flow consists of the following steps: +> +> 1. Initiate the payment process in the drop-in. +> 2. Ask for payment confirmation (e.g., redirect the user to the 3D Secure page). +> 3. Charge the payment on the 3D Secure page. +> 4. Redirect the user to the result (success or failure) page. -### Transaction Events +### Checkout -:::tip + -Before starting any Payment App implementation, consider the transaction events you want to capture and how the app should transition between them. This will help you choose the right webhooks and mutations. +## Modeling the Transaction Flow -::: +### Transaction Events The first step in building our Payment App does not involve any code. To avoid further rework, we need to understand the transaction flow we want to model. The goal is to have a list of events produced by the app and their corresponding webhooks. Let's highlight two key details from the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): -- We want to charge the payment successfully. +- The desired result is charging the payment successfully. - We can't charge the payment without the user's confirmation (3D Secure). -That means the app can't immediately return the `CHARGE_SUCCESS` / `CHARGE_FAILURE` event status. Before it does that, the user needs to confirm the payment. The `CHARGE_ACTION_REQUIRED` event status will represent this step. +In Saleor Transactions API, a successfully charged payment is represented by the [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) event status. If the payment fails, the status is [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure). + +However, before the payment can be charged, we need to confirm it via 3D Secure. This additional action is represented by the [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_action_required) event status. + +:::note -Now, we can assign Saleor transaction event statuses to steps of the [Dummy Payment Gateway flow](#dummy-payment-gateway-flow): +There is a setting in Saleor that can affect this behavior. [Transaction Flow Strategy](api-reference/payments/enums/transaction-flow-strategy-enum) can be set to `CHARGE` or `AUTHORIZATION`. The `CHARGE` strategy allows the payment to be charged immediately, while the `AUTHORIZATION` strategy will require the funds to be authorized first. You can decide on the strategy on per channel basis in the _Configuration_ → _Channels_ → your channel page. + +::: + +Now that we understand the key Saleor transaction event statuses, let's align them with our [Dummy Payment Gateway flow](#dummy-payment-gateway-flow). This mapping will show how each step in our payment process corresponds to a specific Saleor transaction status: | Step | Status | Description | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | 1. Initiate the payment process in the drop-in | --- | --- | | 2. Confirm the payment via 3D Secure | [`CHARGE_ACTION_REQUIRED`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_action_required) | The payment requires additional action to be completed. **The drop-in token is needed to initialize the payment.** | | 3. Charge the payment | --- | --- | -| 3.1 Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) | The payment was successful. | -| 3.2 Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | +| 3a. Charge successful | [`CHARGE_SUCCESS`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_success) | The payment was successful. | +| 3b. Charge failed | [`CHARGE_FAILURE`](api-reference/payments/enums/transaction-event-type-enum.mdx#transactioneventtypeenumcharge_failure) | The payment failed, for example, due to insufficient funds. | | 4. Redirect the user to the result page | --- | --- | ### Webhooks -Now that we've identified the transaction events, we can select the appropriate webhooks to implement. +#### Transaction Initialize Session + +With our transaction events mapped out, let's determine which webhooks we need to implement to handle these events. When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#request-4). @@ -96,39 +113,40 @@ There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_ 1. It is designed for the staff users to charge the payment manually. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. 2. It does not allow the return of the `CHARGE_ACTION_REQUIRED` event status. -That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can emit the `CHARGE_ACTION_REQUIRED` event status, as required. +That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can result in the `CHARGE_ACTION_REQUIRED` event status, as required. + +#### Transaction Process Session -When `TRANSACTION_INITIALIZE_SESSION` returns `CHARGE_ACTION_REQUIRED`, the app must execute this additional action. Depending on the outcome, we want the status to transition to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. The additional [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session) webhook will be responsible for this transition. +When `TRANSACTION_INITIALIZE_SESSION` returns `CHARGE_ACTION_REQUIRED`, the app must execute this additional action. Depending on the outcome, we want the status to change to either `CHARGE_SUCCESS` or `CHARGE_FAILURE`. -Here is the list of webhooks and their corresponding transaction event statuses: +The additional [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session) webhook will be responsible for this transition. -- `TRANSACTION_INITIALIZE_SESSION` -> `CHARGE_ACTION_REQUIRED` -- `TRANSACTION_PROCESS_SESSION` -> `CHARGE_SUCCESS` / `CHARGE_FAILURE` +#### Webhook Sequence -And the sequence diagram of the transaction flow: +The final sequence of webhooks and their corresponding mutations in our payment process is as follows: ```mermaid sequenceDiagram - participant Storefront as Storefront - participant Saleor as Saleor - participant App as Dummy Payment App - Storefront->>Saleor: transactionInitialize - Saleor->>App: TRANSACTION_INITIALIZE_SESSION - App-->>Saleor: { result: CHARGE_ACTION_REQUIRED } - Saleor->>Storefront: { type: CHARGE_ACTION_REQUIRED, ... } - Storefront->>Saleor: transactionProcess - Saleor->>App: TRANSACTION_PROCESS_SESSION - App-->>Saleor: {result: CHARGE_SUCCESS / CHARGE_FAILURE } - Saleor->>Storefront: { type: CHARGE_SUCCESS / CHARGE_FAILURE, ... } + participant Storefront as Storefront + participant Saleor as Saleor + participant App as Dummy Payment App + Storefront->>Saleor: transactionInitialize + Saleor->>App: TRANSACTION_INITIALIZE_SESSION + App-->>Saleor: { result: CHARGE_ACTION_REQUIRED } + Saleor->>Storefront: { type: CHARGE_ACTION_REQUIRED, ... } + Storefront->>Saleor: transactionProcess + Saleor->>App: TRANSACTION_PROCESS_SESSION + App-->>Saleor: {result: CHARGE_SUCCESS / CHARGE_FAILURE } + Saleor->>Storefront: { type: CHARGE_SUCCESS / CHARGE_FAILURE, ... } ``` -Now that we know the webhooks we will implement, we can finally start coding. - ## Creating a Saleor App from Template -Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements.mdx) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't have to manually check all the boxes. +Any app with server-side capabilities can be considered a Saleor App if it matches the requirements described in the [App Requirements](developer/extending/apps/architecture/app-requirements.mdx) document. Thanks to the [App Template](developer/extending/apps/developing-apps/app-template.mdx), we won't have to check all the boxes manually. + +[`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the following sections of the tutorial. -[`saleor-app-template`](https://github.com/saleor/saleor-app-template) is your Next.js starting point for building a Saleor App. It comes with all the necessary scaffolding to get you started. We will explore its features in the following sections of the tutorial. For now, let's clone the template and boot up our app: +For now, let's clone the template and boot up our app: ```bash git clone https://github.com/saleor/saleor-app-template.git @@ -148,21 +166,38 @@ pnpm dev ## Installing the App -To verify that the app is running correctly, we must install it in Saleor. To do that, we need to expose our local development environment to the internet via a tunneling service. If you don't have experience with tunneling, you can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide. +To verify that the app is running correctly, we must install it in Saleor. To do that, we need to expose our local development environment to the internet via a tunneling service. If you don't have experience with tunneling or app installation, you can follow the [Tunneling Apps](developer/extending/apps/developing-with-tunnels.mdx) guide. + +The easiest way to proceed is to install a CLI tunneling tool like [`ngrok`](https://ngrok.com/), and then expose the app on the default port `3000`: + +```bash +ngrok http 3000 +``` + +This command will return a public URL that you can use to install the app in Saleor. + +To do that, go to _Saleor Dashboard_ → _Apps_ → _Install external app_ and paste the URL with the `/api/manifest` suffix. + +After the installation, you should see the app on the _Apps_ list. If you click on it, you will see the App Template's default page. ## Implementing Transaction Initialize Session -### App Manifest +### Updating the App Permissions in the Manifest Our first step will be visiting the `src/pages/api/manifest.ts` directory, where the App Manifest lives. :::info -[App Manifest](developer/extending/apps/architecture/manifest.mdx) is the source of truth for all the app-related metadata, including its webhooks. Saleor calls the `/manifest` API route during the app installation to provide all the necessary information about the app. +[App Manifest](developer/extending/apps/architecture/manifest.mdx) is the source of information about the app, including its webhooks. Saleor calls the `/manifest` API route during the app installation to retrieve all the necessary information about the app. ::: -The `webhooks` array of the App Manifest should be filled with the webhook manifests: +Two fields in the App Manifest require our attention: + +- `permissions` - The list of permissions the app requires to communicate with Saleor correctly (through webhooks and queries). +- `webhooks` - The list of webhooks the app wants to register in Saleor. + +As we can read in the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx#key-concepts) documentation, a Payment App needs [`HANDLE_PAYMENTS`](api-reference/users/enums/permission-enum.mdx#permissionenumhandle_payments) permission to receive transaction webhooks: ```ts import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; @@ -173,67 +208,60 @@ export default createManifestHandler({ // ... const manifest: AppManifest = { name: "Dummy Payment App", - webhooks: [], // 👈 provide webhook manifests here + // highlight-next-line + permissions: ["HANDLE_PAYMENTS"], // ... }; }, }); ``` -Luckily, we don't need to do that manually. +Once we have built our first webhook, we will also add it to the `webhooks` array in the App Manifest. -[Saleor App SDK](developer/extending/apps/developing-apps/app-sdk/overview.mdx), a package included in the template, already provides helper classes for generating the webhooks: `SaleorSyncWebhook` and `SaleorAsyncWebhook`. Webhook instances created from these classes have the `getWebhookManifest` method, which we can use to generate the webhook manifest. +Luckily, we won't need to provide webhook details manually. [Saleor App SDK](developer/extending/apps/developing-apps/app-sdk/overview.mdx), a package included in the template, already provides helper classes for generating the webhooks: `SaleorSyncWebhook` and `SaleorAsyncWebhook`. Webhook instances created from these classes have the `getWebhookManifest` method, which we can use to generate the webhook manifest. ### Declaring `SaleorSyncWebhook` Instance -In this step, we begin implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. +It's time to start implementing a handler for our first webhook: `TRANSACTION_INITIALIZE_SESSION`. -We first need a new file in the `src/pages/api/webhooks` directory, called `transaction-initialize-session.ts`. Then, initialize the `SaleorSyncWebhook` instance (since `TRANSACTION_INITIALIZE_SESSION` is a synchronous webhook): +Let's create a new file in the `src/pages/api/webhooks` directory, called `transaction-initialize-session.ts`. + +Then, initialize the `SaleorSyncWebhook` (since `TRANSACTION_INITIALIZE_SESSION` is a _synchronous_ webhook) instance in that file: ```ts +// src/pages/api/webhooks/transaction-initialize-session.ts import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { saleorApp } from "../../../saleor-app"; export const transactionInitializeSessionWebhook = new SaleorSyncWebhook({ name: "Transaction Initialize Session", - webhookPath: "api/webhooks/transaction-initialize-session", + webhookPath: "/api/webhooks/transaction-initialize-session", event: "TRANSACTION_INITIALIZE_SESSION", apl: saleorApp.apl, query: "", // TODO: Add the subscription query }); ``` -Let's go through all the constructor parameters of the `SaleorSyncWebhook` class: +Here are the required constructor parameters of the `SaleorSyncWebhook` class: - `name` - The name of the webhook. It will be used during the webhook registration process. - `webhookPath` - The path to the webhook. - `event` - The [synchronous webhook event](api-reference/webhooks/enums/webhook-event-type-sync-enum.mdx#values) that the handler will be listening to. +- `event` - The [synchronous webhook event](api-reference/webhooks/enums/webhook-event-type-sync-enum.mdx#values) that the handler will be listening to. - `apl` - The reference to the app's [APL](developer/extending/apps/developing-apps/app-sdk/apl.mdx). Saleor App Template exports it from `src/saleor-app.ts`. - `query` - The query needed to generate the [subscription webhook payload](developer/extending/webhooks/subscription-webhook-payloads.mdx). It is currently empty because we still need to declare our subscription query. -- `unknown` generic attribute - The type of webhook payload the app will receive. Since we don't have the subscription query yet, it is `unknown`. - -### Defining the Subscription Query - -Next, we'll fill in the empty `query` attribute with an actual subscription query. - -With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed.] -#### The `data` Field +Also, we have the `unknown` generic attribute. It represents the type of webhook payload the app will receive. Since we don't have the subscription query yet, it is `unknown`. -Besides our generic payment-related fields, there is one field we are especially interested in: the `data` field. - -There are no requirements for the content of the `data` field, as long as it is a valid JSON object. We can use it to pass any custom information that the payment provider requires. - -In our case, we will use it to pass the token received from the drop-in. In the ["Calling the `transactionInitialize` Mutation"](#calling-the-transactioninitialize-mutation) section, we will provide the token as the mutation variable. In the app, we have to extract it from the payload. +### Defining the Subscription Query -#### The Subscription Query +With [subscription webhook payloads](developer/extending/webhooks/subscription-webhook-payloads.mdx), you can define the shape of the payload the webhook will receive. [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) and the Saleor App SDK ensure the payload is correctly typed. We will fill the `query` attribute with that subscription query. -With that knowledge, let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: +Let's define a subscription query for the `TRANSACTION_INITIALIZE_SESSION` handler: :::info -In Saleor App Template, we use [urql](https://formidable.com/open-source/urql/) to write GraphQL queries. If you prefer a different client, you still should be able to follow along. +In the Saleor App Template, we use [urql](https://formidable.com/open-source/urql/) to write GraphQL queries. If you prefer a different client, you still should be able to follow along. ::: @@ -279,7 +307,7 @@ export const transactionInitializeSessionWebhook = new SaleorSyncWebhook( { name: "Transaction Initialize Session", - webhookPath: "api/webhooks/transaction-initialize-session", + webhookPath: "/api/webhooks/transaction-initialize-session", event: "TRANSACTION_INITIALIZE_SESSION", apl: saleorApp.apl, query: TransactionInitializeSessionSubscription, // 💾 Updated with the subscription query @@ -287,21 +315,52 @@ export const transactionInitializeSessionWebhook = ); ``` -Now that we have the webhook instance, we can use it to populate the `webhooks` array in the App Manifest: +#### `data` Field + +There is one more field we want to add to the `TransactionInitializeSessionPayload` fragment: [`data`](api-reference/payments/objects/transaction-initialize.mdx#transactioninitializedatajson-). + +`data` is a field you can use to pass custom information to the webhook. There are no requirements for the content of the `data` field as long as it is a valid JSON object. You will notice the presence of the `data` field across other payment-related webhooks. + +We will use it to pass [the token received from the drop-in](#dummy-payment-gateway-flow). + +In the [Calling the `transactionInitialize` Mutation](#calling-the-transactioninitialize-mutation) section, we will provide the token as a part of the mutation input. In the app, we receive the token in the `data` field of the payload. + +```ts +const TransactionInitializeSessionPayload = gql` + fragment TransactionInitializeSessionPayload on TransactionInitializeSession { + action { + amount + currency + actionType + } + // highlight-next-line + data + } +`; +``` + +Make sure to regenerate the types after adding the `data` field to the payload fragment. + +#### Updating the App Webhooks in the Manifest + +Moving to populate the `webhooks` array in the App Manifest with the `transactionInitializeSessionWebhook`: ```ts // src/pages/api/manifest.ts import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; import { AppManifest } from "@saleor/app-sdk/types"; -import { transactionInitializeSessionWebhook } from "./webhooks/transaction-initialize-session"; +// highlight-next-line +import { transactionInitializeSessionWebhook } from "./webhooks/transaction-initialize-session"; // 👈 This is our webhook instance export default createManifestHandler({ async manifestFactory({ appBaseUrl }) { // ... const manifest: AppManifest = { name: "Dummy Payment App", + permissions: ["HANDLE_PAYMENTS"], webhooks: [ - transactionInitializeSessionWebhook.getWebhookManifest(appBaseUrl), + // highlight-next-line + transactionInitializeSessionWebhook.getWebhookManifest(appBaseUrl), // 👈 Call `getWebhookManifest` ], // ... }; @@ -311,42 +370,45 @@ export default createManifestHandler({ :::warning -Remember that modifying an existing subscription query or adding a new webhook requires manual webhook update in Saleor API. The easiest way to do it is to reinstall the app. You can read more about this behavior in [How to Update App Webhooks](developer/extending/apps/updating-app-webhooks.mdx). +Beware that modifying a subscription query for a registered webhook or adding a new webhook requires manual webhook update in Saleor API. The easiest way to do it is to reinstall the app. You can read more about this behavior in [How to Update App Webhooks](developer/extending/apps/updating-app-webhooks.mdx). ::: ### Creating the Webhook Handler -The last step is to create the webhook handler. The handler is a function that will be called when the webhook is triggered. `saleor-app-sdk` handler is a decorated Next.js API ([pages](https://nextjs.org/docs/pages)) route handler. The extra bit is the Saleor context that is passed to the handler. +The handler is a function that will be called when the webhook is triggered. `saleor-app-sdk` handler is a decorated Next.js API ([pages](https://nextjs.org/docs/pages)) route handler. -Let's extend our `transaction-initialize-session.ts` file with the handler boilerplate: +Let's go back to the `src/pages/api/webhooks/transaction-initialize-session.ts` file and extend it with the handler: ```ts +// src/pages/api/webhooks/transaction-initialize-session.ts + +// ... export default transactionInitializeSessionWebhook.createHandler( (req, res, ctx) => { const { payload, event, baseUrl, authData } = ctx; - // TODO: Implement the handler + // TODO: Implement the logic return res.status(200).end(); } ); ``` -The `ctx` object contains the following properties: +As you can see, we are using the `createHandler` method of the `transactionInitializeSessionWebhook` instance. This method takes a handler function as an argument. The handler function receives three arguments: `req`, `res`, and `ctx`. + +While the first two are the standard Next.js request and response objects, the third one is the Saleor context. It contains the following properties: -- `payload` - The type-safe payload received from the webhook. +- `payload` - Type-safe subscription webhook payload. - `event` - Name of the event that triggered the webhook. -- `baseUrl` - The base URL of the app. If you need to register the app or webhook in an external service, you can use this URL. -- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` you can use to query Saleor API. +- `baseUrl` - The base URL of the app. If you need to register the app or a webhook in an external service, you can use this URL. +- `authData` - The [authentication data](developer/extending/apps/developing-apps/app-sdk/apl.mdx#authdata) passed from Saleor to the app. Among other things, it contains the `token` you can use to query Saleor API. We won't need it in this tutorial. Since we are implementing the Dummy Payment Gateway, our handler logic will be basic. We will just log the payload and return the `CHARGE_ACTION_REQUIRED` event status. The only thing we need to remember is that the webhook response must adhere to the [webhook response format](developer/extending/webhooks/synchronous-events/transaction.mdx#response-4): - - :::info -Here is what a real-world `TRANSACTION_INITIALIZE_SESSION` webhook handler might do: +A real-world `TRANSACTION_INITIALIZE_SESSION` webhook handler might: - Retrieve some kind of token from the `payload.data` to identify the payment. - Transform the payload into a format expected by the payment provider. @@ -362,6 +424,8 @@ export default transactionInitializeSessionWebhook.createHandler( console.log("Transaction Initialize Session payload:", payload); + // validatePayment(payload.data.token); // This function could validate the token + const randomPspReference = crypto.randomUUID(); // Generate a random PSP reference return res.status(200).json({ @@ -374,9 +438,80 @@ export default transactionInitializeSessionWebhook.createHandler( ); ``` -By returning the `CHARGE_ACTION_REQUIRED` event status, we are informing Saleor that the payment requires additional action to be completed. We will perform this action in the next webhook handler. +By returning the `CHARGE_ACTION_REQUIRED` event status, we inform Saleor that the payment requires additional action. We will perform this action in the next webhook handler. + +Besides the `result` field, we also return the `amount` and `pspReference` fields. The `amount` is the transaction amount, and the `pspReference` is a unique identifier of the payment in the payment provider system. Since we are not integrating with an actual payment provider, we generate a random `pspReference`. + +The last thing we must add to the bottom of the handler is the `config` object with `bodyParser` set to `false`. This is necessary for App-SDK to verify the Saleor webhook signature: + +```ts +export const config = { + api: { + bodyParser: false, + }, +}; +``` + +Here is the full content of the `transaction-initialize-session.ts` file: + +```ts +import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; +import { saleorApp } from "../../../saleor-app"; +import { TransactionInitializeSessionPayloadFragment } from "../../../../generated/graphql"; +import { gql } from "urql"; + +const TransactionInitializeSessionPayload = gql` + fragment TransactionInitializeSessionPayload on TransactionInitializeSession { + action { + amount + currency + actionType + } + data + } +`; + +const TransactionInitializeSessionSubscription = gql` + # Payload fragment must be included in the root query + ${TransactionInitializeSessionPayload} + subscription TransactionInitializeSession { + event { + ...TransactionInitializeSessionPayload + } + } +`; + +export const transactionInitializeSessionWebhook = + new SaleorSyncWebhook({ + name: "Transaction Initialize Session", + webhookPath: "/api/webhooks/transaction-initialize-session", + event: "TRANSACTION_INITIALIZE_SESSION", + apl: saleorApp.apl, + query: TransactionInitializeSessionSubscription, + }); + +export default transactionInitializeSessionWebhook.createHandler( + (req, res, ctx) => { + const { payload, event, baseUrl, authData } = ctx; + + console.log("Transaction Initialize Session payload:", payload); + + const randomPspReference = crypto.randomUUID(); + + return res.status(200).json({ + result: "CHARGE_ACTION_REQUIRED", + amount: payload.action.amount, + pspReference: randomPspReference, + }); + } +); -Besides the `result` field, we also return the `amount` and `pspReference` fields. The `amount` is the amount of the transaction, and the `pspReference` is a unique identifier of the payment in the payment provider system. Since we are not integrating with a real payment provider, we generate a random `pspReference`. +export const config = { + api: { + bodyParser: false, + }, +}; +``` ### Calling the `transactionInitialize` Mutation @@ -385,10 +520,14 @@ Assuming you installed the app in Saleor, we can finally try out our webhook han Here is what the mutation looks like: ```graphql -mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { +mutation TransactionInitialize( + $checkoutId: ID! + $data: JSON! + $paymentGatewayId: String! +) { transactionInitialize( id: $checkoutId - paymentGateway: { id: $paymentGateway } + paymentGateway: { id: $paymentGatewayId, data: $data } ) { transaction { id @@ -406,11 +545,13 @@ mutation TransactionInitialize($checkoutId: ID!, $paymentGateway: String!) { } ``` -The mutation requires two variables: `checkoutId` and `paymentGateway`. +The mutation requires `checkoutId`, `data` and `paymentGatewayId`. + +- `checkoutId` is the ID of [the checkout](#checkout) we want to pay for. -The `checkoutId` is the ID of the checkout that we want to pay for. +- `data` is that JSON object for additional information [we mentioned earlier](#data-field). In our case, it will contain the token received from the fictional drop-in. -The `paymentGateway` is the ID of the payment gateway that we want to use. We can see what payment gateways are available for the checkout by requesting the `availablePaymentGateways` field on the `checkout` query: +- `paymentGatewayId` is the ID of the payment gateway that we want to use. We can see what payment gateways are available for the checkout by requesting the `availablePaymentGateways` field on the `checkout` query: ```graphql query GetCheckout($id: ID!) { @@ -423,21 +564,31 @@ query GetCheckout($id: ID!) { } ``` -If we installed the app correctly, we should see the "Dummy Payment Gateway" in the list of available payment gateways. The `id` will be drawn from the App Manifest's `id` field. +If we installed the app correctly, we should see the "Dummy Payment Gateway" in the list of available payment gateways. The `id` will be drawn from the App Manifest's `id` field (in the App Template, the default id is `saleor.app`). -If we have the `checkoutId` and `paymentGateway`, we can now call the `transactionInitialize` mutation. In the response, we should see the created transaction and the transaction event with the `CHARGE_ACTION_REQUIRED` type: +With `checkoutId`, `data` and `paymentGatewayId`, we can now call the `transactionInitialize` mutation: - +```json +{ + "checkoutId": "Q2hlY2tvdXQ6ZjVhMTkzMjUtMjY3My00MTU0LThjM2QtYjE1OThlYzVlZjc3", + "data": { + "token": "dummy-drop-in-token" + }, + "paymentGatewayId": "saleor.app" +} +``` + +In the response, we should see the created transaction and the transaction event with the `CHARGE_ACTION_REQUIRED` type: ```json { "data": { "transactionInitialize": { "transaction": { - "id": "UHJvamVjdFRy" + "id": "VHJhbnNhY3Rpb25JdGVtOjhiZDY0NTA2LTRlYWYtNGZmYS05ZmRjLTY1MTY5MTc3ZTg2MA==" }, "transactionEvent": { - "id": "VHJhbnNhY3Rpb25FdmVudDoz", + "id": "VHJhbnNhY3Rpb25FdmVudDo0MDkx", "type": "CHARGE_ACTION_REQUIRED" }, "errors": [] @@ -446,7 +597,20 @@ If we have the `checkoutId` and `paymentGateway`, we can now call the `transacti } ``` -The app's console should also log the payload received from the webhook. +The app's console should also log the payload received from the webhook: + +```json +{ + "action": { + "amount": 100, + "currency": "USD", + "actionType": "CHARGE" + }, + "data": { + "token": "dummy-drop-in-token" + } +} +``` ## Implementing Transaction Process Session @@ -492,14 +656,38 @@ const TransactionProcessSessionSubscription = gql` And then, create the `SaleorSyncWebhook` instance, as well as the webhook handler: ```ts +import { TransactionProcessSessionPayloadFragment } from "../../../../generated/graphql"; // Import the generated payload type +import { gql } from "urql"; +// highlight-start import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import { saleorApp } from "../../../saleor-app"; -// ... +// highlight-end + +// 💡 Remember to regenerate the types after adding the new subscription query +const TransactionProcessSessionPayload = gql` + fragment TransactionProcessSessionPayload on TransactionProcessSession { + action { + amount + currency + actionType + } + } +`; + +const TransactionProcessSessionSubscription = gql` + ${TransactionProcessSessionPayload} + subscription TransactionProcessSession { + event { + ...TransactionProcessSessionPayload + } + } +`; +// highlight-start export const transactionProcessSessionWebhook = new SaleorSyncWebhook({ name: "Transaction Process Session", - webhookPath: "api/webhooks/transaction-process-session", + webhookPath: "/api/webhooks/transaction-process-session", event: "TRANSACTION_PROCESS_SESSION", apl: saleorApp.apl, query: TransactionProcessSessionSubscription, @@ -518,18 +706,29 @@ export default transactionProcessSessionWebhook.createHandler( }); } ); + +export const config = { + api: { + bodyParser: false, + }, +}; +// highlight-end ``` +Once again, our handler is simple. We only log the payload and return the `CHARGE_SUCCESS` event status, no questions asked. + :::info -Here is what a real-world `TRANSACTION_PROCESS_SESSION` webhook handler might do: +A real-world `TRANSACTION_PROCESS_SESSION` webhook handler might: -- Call the payment provider API to check the payment status. -- Process the response from the payment provider and return the `CHARGE_SUCCESS` or `CHARGE_FAILURE` event status. +- Call the payment provider API to confirm the payment status. +- Return either `CHARGE_SUCCESS` or `CHARGE_FAILURE` based on the response. ::: -If the handler would perform a logic that can fail, we should try to catch the error and return the `CHARGE_FAILURE` event status: +#### Error Handling + +If the handler performs a logic that could fail, we should try to catch the error and, in that case, return the `CHARGE_FAILURE` event status: ```ts export default transactionProcessSessionWebhook.createHandler( @@ -544,6 +743,7 @@ export default transactionProcessSessionWebhook.createHandler( return res.status(200).json({ result: "CHARGE_SUCCESS", amount: payload.action.amount, + // TODO: CHECK IF THESE ARE THE SAME PSP REFERENCES pspReference: crypto.randomUUID(), }); } catch (error) { @@ -557,6 +757,8 @@ export default transactionProcessSessionWebhook.createHandler( ); ``` +#### Updating the App Manifest + The last step is to update the App Manifest with the new webhook: ```ts @@ -583,7 +785,17 @@ export default createManifestHandler({ }); ``` -Once again, remember to update the app's webhooks or reinstall the app in Saleor. Otherwise, the new webhook handler won't be called. +Once again, **remember to update the app's webhooks or reinstall the app in Saleor**. Otherwise, the new webhook handler won't be called on `transactionProcess`. + +:::warning + +When installing the app back, you may get "**App with the same identifier is already installed**" error. + +Saleor throws this error because the process of fully uninstalling the app is handled by an asynchronous Saleor worker. This means it can take some time before you can install the app with the same identifier again. + +If you encounter this error, you can temporarily change the app's `id` in the App Manifest. + +::: ### Calling the `transactionProcess` Mutation @@ -631,10 +843,14 @@ In response, we should receive the processed transaction and the corresponding t And that concludes the implementation of the Dummy Payment Gateway. With just two webhooks, we managed to model a 3D Secure payment flow. +For most checkouts, placing payment will be the last step. That means you could now [complete the checkout](api-reference/checkout/mutations/checkout-complete.mdx) and place the order 🎉. + ## Next steps -Once you have covered the basics, you can extend the app with more features. Here are some ideas: +Congratulations on building your first Saleor Payment App! + +Its scope doesn't have to end here. You can further extend the app with additional features: -- If your payment provider requires additional operation even before the payment is initialized (for example, to render the drop-in), you might be interested in reading about [`PAYMENT_GATEWAY_INITIALIZE_SESSION`](developer/payments.mdx#initialize-payment-gateway). -- If you want to allow users to save their payment methods for future use, you should implement the [Stored Payment Methods API](developer/payments.mdx#stored-payment-methods). +- If your payment provider requires additional operation before the payment is even initialized (for example, to render the drop-in), you might be interested in reading about the [`PAYMENT_GATEWAY_INITIALIZE_SESSION`](developer/payments.mdx#initialize-payment-gateway) webhook. +- To allow users to save their payment methods for future use, you should implement the [Stored Payment Methods API](developer/payments.mdx#stored-payment-methods). - If your payment provider needs to report the transaction status to Saleor asynchronously, you can use the [`transactionEventReport` mutation](developer/payments.mdx#reporting-actions-for-transactions). From 6c7f6e4cb32d2f4b4155b74af029d8267ffa2708 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 19 Jul 2024 14:01:43 +0200 Subject: [PATCH 11/13] add the checkout bit --- docs/developer/extending/apps/building-payment-app.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 281b7ecf9..cba2236af 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -66,7 +66,7 @@ Here is how it looks: ### Checkout - +In Saleor, the payment process is a part of [checkout](developer/checkout/overview.mdx). In this tutorial, we will focus on the payment part only. We will assume that the checkout is already created and the user is ready to pay. ## Modeling the Transaction Flow From 257bccc479e62576d82f46ab25d8a8d5caf8352a Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 19 Jul 2024 14:07:21 +0200 Subject: [PATCH 12/13] fix broken link --- docs/developer/extending/apps/building-payment-app.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index cba2236af..4d0df37e1 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -85,7 +85,7 @@ However, before the payment can be charged, we need to confirm it via 3D Secure. :::note -There is a setting in Saleor that can affect this behavior. [Transaction Flow Strategy](api-reference/payments/enums/transaction-flow-strategy-enum) can be set to `CHARGE` or `AUTHORIZATION`. The `CHARGE` strategy allows the payment to be charged immediately, while the `AUTHORIZATION` strategy will require the funds to be authorized first. You can decide on the strategy on per channel basis in the _Configuration_ → _Channels_ → your channel page. +There is a setting in Saleor that can affect this behavior. [Transaction Flow Strategy](api-reference/payments/enums/transaction-flow-strategy-enum.mdx) can be set to `CHARGE` or `AUTHORIZATION`. The `CHARGE` strategy allows the payment to be charged immediately, while the `AUTHORIZATION` strategy will require the funds to be authorized first. You can decide on the strategy on per channel basis in the _Configuration_ → _Channels_ → your channel page. ::: From c477866cb040a10e7846a9b497627d15251c9479 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 23 Jul 2024 09:52:32 +0200 Subject: [PATCH 13/13] feedback --- .../extending/apps/building-payment-app.mdx | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/developer/extending/apps/building-payment-app.mdx b/docs/developer/extending/apps/building-payment-app.mdx index 4d0df37e1..447e1c9ee 100644 --- a/docs/developer/extending/apps/building-payment-app.mdx +++ b/docs/developer/extending/apps/building-payment-app.mdx @@ -25,11 +25,17 @@ A Payment App is [a Saleor App](developer/extending/apps/overview.mdx) that foll That interface consists of [synchronous webhooks](developer/extending/webhooks/synchronous-events/overview.mdx). This feature allows Saleor to suspend the resolution of an API call until Saleor receives a response from the app in the correct format. Saleor can then pass that response as the value of the requested field. +:::info + +Saleor supports two types of webhooks: synchronous and asynchronous. Synchronous events are sent immediately after the event happens during GraphQL requests. Asynchronous events are sent after request processing is finished. You can read more about them in the [Webhooks](developer/extending/webhooks/overview.mdx) article. + +::: + We will look at what webhooks we must implement in [the "Webhooks" section](#webhooks). Each payment service has its unique payment flow, so Saleor needs a flexible API to support them. This flexibility is the core idea behind the [Transactions API](developer/extending/webhooks/synchronous-events/transaction.mdx), which we will use for processing payments. -Our fictional payment gateway is a simple one, so we won't cover the entire API in this tutorial. For real payment providers, you may need to implement additional webhooks and mutations. +Our fictional payment gateway is a simple one, so we won't cover the entire API in this tutorial. For real payment providers, the flow may differ. Gateway X may require multiple webhooks to capture the payment process, while Gateway Y may only need one. :::info @@ -43,7 +49,7 @@ If you need a reference point for integrating with real payment providers, you c ### What is a Transaction -A transaction represents a payment event that happened in Order or Checkout. These events include actions like charging a payment, authorizing or refunding it. You can see the complete list of events in the [`TransactionEventTypeEnum`](api-reference/payments/enums/transaction-event-type-enum.mdx). +A transaction represents a payment instance created in Order or Checkout. It holds a list of events that make up the payment process. Each event has a type that describes the action taken on the transaction. You can see the complete list of events in the [`TransactionEventTypeEnum`](api-reference/payments/enums/transaction-event-type-enum.mdx). Besides the events, a transaction also contains other payment information, like the amount or currency. @@ -66,7 +72,7 @@ Here is how it looks: ### Checkout -In Saleor, the payment process is a part of [checkout](developer/checkout/overview.mdx). In this tutorial, we will focus on the payment part only. We will assume that the checkout is already created and the user is ready to pay. +Transaction API can be used both in Checkout and Order. In this tutorial, we will focus on the payment part only and assume that [the checkout is already created](developer/checkout/overview.mdx#creating-a-checkout-session) and the user is ready to pay. ## Modeling the Transaction Flow @@ -106,14 +112,16 @@ Now that we understand the key Saleor transaction event statuses, let's align th With our transaction events mapped out, let's determine which webhooks we need to implement to handle these events. -When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that two webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge) and [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#request-4). +When exploring the [Transaction Events](developer/extending/webhooks/synchronous-events/transaction.mdx) documentation, you may notice that three webhooks can result in the `CHARGE_SUCCESS` event: [`TRANSACTION_CHARGE_REQUESTED`](developer/extending/webhooks/synchronous-events/transaction.mdx#transaction-charge), [`TRANSACTION_INITIALIZE_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#initialize-transaction-session) and [`TRANSACTION_PROCESS_SESSION`](developer/extending/webhooks/synchronous-events/transaction.mdx#process-transaction-session). There are two reasons for why **we are not going** with the `TRANSACTION_CHARGE_REQUESTED` webhook: 1. It is designed for the staff users to charge the payment manually. Since we want a checkout user (possibly unauthenticated) to be able to pay for the order, it is not a good fit. 2. It does not allow the return of the `CHARGE_ACTION_REQUIRED` event status. -That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions. It can result in the `CHARGE_ACTION_REQUIRED` event status, as required. +`TRANSACTION_PROCESS_SESSION` is also not a match. It is triggered when `transactionProcess` mutation is called, but only when the `TRANSACTION_INITIALIZE_SESSION` was called first and returned `CHARGE_ACTION_REQUIRED` or `AUTHORIZE_ACTION_REQUIRED` event status. This webhook will be useful for the second step of our payment process. + +That leaves us with the `TRANSACTION_INITIALIZE_SESSION` webhook. It is triggered on the [`transactionInitialize`](api-reference/payments/mutations/transaction-initialize.mdx) mutation which can be used without any permissions (unless you are using [the `action` field](api-reference/payments/mutations/transaction-initialize.mdx#transactioninitializeactiontransactionflowstrategyenum-)). It can result in the `CHARGE_ACTION_REQUIRED` event status, as required. #### Transaction Process Session @@ -127,17 +135,18 @@ The final sequence of webhooks and their corresponding mutations in our payment ```mermaid sequenceDiagram - participant Storefront as Storefront - participant Saleor as Saleor - participant App as Dummy Payment App + participant Storefront + participant Saleor + participant Dummy Payment App Storefront->>Saleor: transactionInitialize - Saleor->>App: TRANSACTION_INITIALIZE_SESSION - App-->>Saleor: { result: CHARGE_ACTION_REQUIRED } + Saleor->>Dummy Payment App: TRANSACTION_INITIALIZE_SESSION + Dummy Payment App-->>Saleor: { result: CHARGE_ACTION_REQUIRED } Saleor->>Storefront: { type: CHARGE_ACTION_REQUIRED, ... } Storefront->>Saleor: transactionProcess - Saleor->>App: TRANSACTION_PROCESS_SESSION - App-->>Saleor: {result: CHARGE_SUCCESS / CHARGE_FAILURE } + Saleor->>Dummy Payment App: TRANSACTION_PROCESS_SESSION + Dummy Payment App-->>Saleor: {result: CHARGE_SUCCESS / CHARGE_FAILURE } Saleor->>Storefront: { type: CHARGE_SUCCESS / CHARGE_FAILURE, ... } + Note over Dummy Payment App: This process is modeled after a 3D Secure flow. ``` ## Creating a Saleor App from Template @@ -328,18 +337,18 @@ In the [Calling the `transactionInitialize` Mutation](#calling-the-transactionin ```ts const TransactionInitializeSessionPayload = gql` fragment TransactionInitializeSessionPayload on TransactionInitializeSession { - action { - amount - currency - actionType - } - // highlight-next-line - data - } + action { + amount + currency + actionType + } + // highlight-next-line + data + } `; ``` -Make sure to regenerate the types after adding the `data` field to the payload fragment. +Make sure to regenerate the types (by calling `pnpm generate`) after adding the `data` field to the payload fragment. #### Updating the App Webhooks in the Manifest @@ -432,7 +441,6 @@ export default transactionInitializeSessionWebhook.createHandler( result: "CHARGE_ACTION_REQUIRED", amount: payload.action.amount, // `payload` is typed thanks to the generated types pspReference: randomPspReference, - // TODO: CHECK IF OTHER FIELDS ARE NEEDED }); } ); @@ -722,7 +730,7 @@ Once again, our handler is simple. We only log the payload and return the `CHARG A real-world `TRANSACTION_PROCESS_SESSION` webhook handler might: - Call the payment provider API to confirm the payment status. -- Return either `CHARGE_SUCCESS` or `CHARGE_FAILURE` based on the response. +- Return either `CHARGE_SUCCESS`, `CHARGE_FAILURE` or `CHARGE_ACTION_REQUIRED` (if there are multiple checks required). ::: @@ -743,7 +751,6 @@ export default transactionProcessSessionWebhook.createHandler( return res.status(200).json({ result: "CHARGE_SUCCESS", amount: payload.action.amount, - // TODO: CHECK IF THESE ARE THE SAME PSP REFERENCES pspReference: crypto.randomUUID(), }); } catch (error) {