Notify event webhook for customer emails (#408)

* Support Notify webhook for account operations

* Fix the comment

* Do not expose internal types of the event

* Remove debug message
This commit is contained in:
Krzysztof Wolski 2023-04-19 15:56:45 +02:00 committed by GitHub
parent a3636f73ef
commit ede7a2e808
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 479 additions and 86 deletions

View file

@ -0,0 +1,4 @@
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
const { host, "x-forwarded-proto": protocol = "http" } = headers;
return `${protocol}://${host}`;
};

View file

@ -0,0 +1,39 @@
import { Client, gql } from "urql";
import { WebhookCreateMutationDocument, WebhookEventTypeEnum } from "../../generated/graphql";
import { notifyWebhook } from "../pages/api/webhooks/notify";
const webhookCreateMutation = gql`
mutation webhookCreateMutation($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
webhook {
id
name
isActive
}
errors {
field
message
}
}
}
`;
interface RegisterNotifyWebhookArgs {
client: Client;
baseUrl: string;
}
export const registerNotifyWebhook = async ({ client, baseUrl }: RegisterNotifyWebhookArgs) => {
const manifest = notifyWebhook.getWebhookManifest(baseUrl);
return await client
.mutation(WebhookCreateMutationDocument, {
input: {
name: manifest.name,
targetUrl: manifest.targetUrl,
events: [WebhookEventTypeEnum.NotifyUser],
isActive: true,
},
})
.toPromise();
};

View file

@ -18,7 +18,7 @@ const useStyles = makeStyles((theme) => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 20, gap: 20,
maxWidth: 600, maxWidth: 700,
}, },
}; };
}); });

View file

@ -8,6 +8,7 @@ import {
OrderFullyPaidWebhookPayloadFragment, OrderFullyPaidWebhookPayloadFragment,
InvoiceSentWebhookPayloadFragment, InvoiceSentWebhookPayloadFragment,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { NotifyEventPayload } from "../../pages/api/webhooks/notify";
const exampleOrderPayload: OrderDetailsFragment = { const exampleOrderPayload: OrderDetailsFragment = {
id: "T3JkZXI6NTdiNTBhNDAtYzRmYi00YjQzLWIxODgtM2JhZmRlMTc3MGQ5", id: "T3JkZXI6NTdiNTBhNDAtYzRmYi00YjQzLWIxODgtM2JhZmRlMTc3MGQ5",
@ -136,6 +137,116 @@ const invoiceSentPayload: InvoiceSentWebhookPayloadFragment = {
order: exampleOrderPayload, order: exampleOrderPayload,
}; };
const accountConfirmationPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
first_name: "John",
last_name: "Doe",
is_staff: false,
is_active: false,
private_metadata: {},
metadata: {},
language_code: "en",
},
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
confirm_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountPasswordResetPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
first_name: "John",
last_name: "Doe",
is_staff: false,
is_active: false,
private_metadata: {},
metadata: {},
language_code: "en",
},
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
reset_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountChangeEmailRequestPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
first_name: "John",
last_name: "Doe",
is_staff: false,
is_active: false,
private_metadata: {},
metadata: {},
language_code: "en",
},
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
old_email: "test@example.com1",
new_email: "new.email@example.com1",
redirect_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountChangeEmailConfirmPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
first_name: "John",
last_name: "Doe",
is_staff: false,
is_active: false,
private_metadata: {},
metadata: {},
language_code: "en",
},
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountDeletePayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
first_name: "John",
last_name: "Doe",
is_staff: false,
is_active: false,
private_metadata: {},
metadata: {},
language_code: "en",
},
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
delete_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
export const examplePayloads: Record<MessageEventTypes, any> = { export const examplePayloads: Record<MessageEventTypes, any> = {
ORDER_CREATED: orderCreatedPayload, ORDER_CREATED: orderCreatedPayload,
ORDER_CONFIRMED: orderConfirmedPayload, ORDER_CONFIRMED: orderConfirmedPayload,
@ -143,4 +254,9 @@ export const examplePayloads: Record<MessageEventTypes, any> = {
ORDER_FULFILLED: orderFulfilledPayload, ORDER_FULFILLED: orderFulfilledPayload,
ORDER_FULLY_PAID: orderFullyPaidPayload, ORDER_FULLY_PAID: orderFullyPaidPayload,
INVOICE_SENT: invoiceSentPayload, INVOICE_SENT: invoiceSentPayload,
ACCOUNT_CONFIRMATION: accountConfirmationPayload,
ACCOUNT_PASSWORD_RESET: accountPasswordResetPayload,
ACCOUNT_CHANGE_EMAIL_REQUEST: accountChangeEmailRequestPayload,
ACCOUNT_CHANGE_EMAIL_CONFIRM: accountChangeEmailConfirmPayload,
ACCOUNT_DELETE: accountDeletePayload,
}; };

View file

@ -1,5 +1,3 @@
import { AsyncWebhookEventType } from "@saleor/app-sdk/types";
export const messageEventTypes = [ export const messageEventTypes = [
"ORDER_CREATED", "ORDER_CREATED",
"ORDER_FULFILLED", "ORDER_FULFILLED",
@ -7,11 +5,14 @@ export const messageEventTypes = [
"ORDER_CANCELLED", "ORDER_CANCELLED",
"ORDER_FULLY_PAID", "ORDER_FULLY_PAID",
"INVOICE_SENT", "INVOICE_SENT",
"ACCOUNT_CONFIRMATION",
"ACCOUNT_PASSWORD_RESET",
"ACCOUNT_CHANGE_EMAIL_REQUEST",
"ACCOUNT_CHANGE_EMAIL_CONFIRM",
"ACCOUNT_DELETE",
] as const; ] as const;
type Subset<K, T extends K> = T; export type MessageEventTypes = (typeof messageEventTypes)[number];
export type MessageEventTypes = Subset<AsyncWebhookEventType, (typeof messageEventTypes)[number]>;
export const messageEventTypesLabels: Record<MessageEventTypes, string> = { export const messageEventTypesLabels: Record<MessageEventTypes, string> = {
ORDER_CREATED: "Order created", ORDER_CREATED: "Order created",
@ -20,4 +21,9 @@ export const messageEventTypesLabels: Record<MessageEventTypes, string> = {
ORDER_CANCELLED: "Order cancelled", ORDER_CANCELLED: "Order cancelled",
ORDER_FULLY_PAID: "Order fully paid", ORDER_FULLY_PAID: "Order fully paid",
INVOICE_SENT: "Invoice sent", INVOICE_SENT: "Invoice sent",
ACCOUNT_CONFIRMATION: "Customer account confirmation",
ACCOUNT_PASSWORD_RESET: "Customer account password reset",
ACCOUNT_CHANGE_EMAIL_REQUEST: "Customer account change email request",
ACCOUNT_CHANGE_EMAIL_CONFIRM: "Customer account change email confirmation",
ACCOUNT_DELETE: "Customer account delete request",
}; };

View file

@ -1,79 +1,76 @@
import { MessageEventTypes } from "../event-handlers/message-event-types"; import { MessageEventTypes } from "../event-handlers/message-event-types";
const addressSection = ` const addressSection = `<mj-section>
<mj-section> <mj-column>
<mj-column> <mj-table>
<mj-table> <thead>
<thead> <tr>
<tr> <th>
<th> Billing address
Billing address </th>
</th> <th>
<th> Shipping address
Shipping address </th>
</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <tr>
<tr> <td>
<td> {{#if order.billingAddress}}
{{#if order.billingAddress}} {{ order.billingAddress.streetAddress1 }}
{{ order.billingAddress.streetAddress1 }} {{else}}
{{else}} No billing address
No billing address {{/if}}
{{/if}} </td>
</td> <td>
<td> {{#if order.shippingAddress}}
{{#if order.shippingAddress}} {{ order.shippingAddress.streetAddress1}}
{{ order.shippingAddress.streetAddress1}} {{else}}
{{else}} No shipping required
No shipping required {{/if}}
{{/if}} </td>
</td> </tr>
</tr> </tbody>
</tbody> </mj-table>
</mj-table> </mj-column>
</mj-column> </mj-section>
</mj-section>
`; `;
const orderLinesSection = ` const orderLinesSection = `<mj-section>
<mj-section> <mj-column>
<mj-column> <mj-table>
<mj-table> <tbody>
<tbody> {{#each order.lines }}
{{#each order.lines }}
<tr>
<td>
{{ this.quantity }} x {{ this.productName }} - {{ this.variantName }}
</td>
<td align="right">
{{ this.totalPrice.gross.amount }} {{ this.totalPrice.gross.currency }}
</td>
</tr>
{{/each}}
<tr> <tr>
<td> <td>
{{ this.quantity }} x {{ this.productName }} - {{ this.variantName }}
</td> </td>
<td align="right"> <td align="right">
Shipping: {{ order.shippingPrice.gross.amount }} {{ order.shippingPrice.gross.currency }} {{ this.totalPrice.gross.amount }} {{ this.totalPrice.gross.currency }}
</td> </td>
</tr> </tr>
<tr> {{/each}}
<td> <tr>
</td> <td>
<td align="right"> </td>
Total: {{ order.total.gross.amount }} {{ order.total.gross.currency }} <td align="right">
</td> Shipping: {{ order.shippingPrice.gross.amount }} {{ order.shippingPrice.gross.currency }}
</tr> </td>
</tbody> </tr>
</mj-table> <tr>
</mj-column> <td>
</mj-section> </td>
<td align="right">
Total: {{ order.total.gross.amount }} {{ order.total.gross.currency }}
</td>
</tr>
</tbody>
</mj-table>
</mj-column>
</mj-section>
`; `;
const defaultOrderCreatedMjmlTemplate = ` const defaultOrderCreatedMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -90,8 +87,7 @@ const defaultOrderCreatedMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultOrderFulfilledMjmlTemplate = ` const defaultOrderFulfilledMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -108,8 +104,7 @@ const defaultOrderFulfilledMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultOrderConfirmedMjmlTemplate = ` const defaultOrderConfirmedMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -126,8 +121,7 @@ const defaultOrderConfirmedMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultOrderFullyPaidMjmlTemplate = ` const defaultOrderFullyPaidMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -144,8 +138,7 @@ const defaultOrderFullyPaidMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultOrderCancelledMjmlTemplate = ` const defaultOrderCancelledMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -162,8 +155,7 @@ const defaultOrderCancelledMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultInvoiceSentMjmlTemplate = ` const defaultInvoiceSentMjmlTemplate = `<mjml>
<mjml>
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
@ -178,6 +170,93 @@ const defaultInvoiceSentMjmlTemplate = `
</mj-body> </mj-body>
</mjml>`; </mjml>`;
const defaultAccountConfirmationMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hi {{user.first_name}}!
</mj-text>
<mj-text>
Your account has been created. Please follow the link to activate it:
</mj-text>
<mj-button href="{{confirm_url}}" background-color="black" color="white" padding-top="50px" inner-padding="20px" width="70%">
Activate the account
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>`;
const defaultAccountPasswordResetMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hi {{user.first_name}}!
</mj-text>
<mj-text>
Password reset has been requested. Please follow the link to proceed:
</mj-text>
<mj-button href="{{confirm_url}}" background-color="black" color="white" padding-top="50px" inner-padding="20px" width="70%">
Reset the password
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>`;
const defaultAccountChangeEmailRequestMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hi {{user.first_name}}!
</mj-text>
<mj-text>
Email address change has been requested. If you want to confirm changing the email address to {{new_email}}, please follow the link:
</mj-text>
<mj-button href="{{redirect_url}}" background-color="black" color="white" padding-top="50px" inner-padding="20px" width="70%">
Change the email
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>`;
const defaultAccountChangeEmailConfirmationMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hi {{user.first_name}}!
</mj-text>
<mj-text>
Email address change has been confirmed.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>`;
const defaultAccountDeleteMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hi {{user.first_name}}!
</mj-text>
<mj-text>
Account deletion has been requested. If you want to confirm, please follow the link:
</mj-text>
<mj-button href="{{redirect_url}}" background-color="black" color="white" padding-top="50px" inner-padding="20px" width="70%">
Delete the account
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>`;
export const defaultMjmlTemplates: Record<MessageEventTypes, string> = { export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ORDER_CREATED: defaultOrderCreatedMjmlTemplate, ORDER_CREATED: defaultOrderCreatedMjmlTemplate,
ORDER_FULFILLED: defaultOrderFulfilledMjmlTemplate, ORDER_FULFILLED: defaultOrderFulfilledMjmlTemplate,
@ -185,6 +264,11 @@ export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ORDER_FULLY_PAID: defaultOrderFullyPaidMjmlTemplate, ORDER_FULLY_PAID: defaultOrderFullyPaidMjmlTemplate,
ORDER_CANCELLED: defaultOrderCancelledMjmlTemplate, ORDER_CANCELLED: defaultOrderCancelledMjmlTemplate,
INVOICE_SENT: defaultInvoiceSentMjmlTemplate, INVOICE_SENT: defaultInvoiceSentMjmlTemplate,
ACCOUNT_CONFIRMATION: defaultAccountConfirmationMjmlTemplate,
ACCOUNT_PASSWORD_RESET: defaultAccountPasswordResetMjmlTemplate,
ACCOUNT_CHANGE_EMAIL_REQUEST: defaultAccountChangeEmailRequestMjmlTemplate,
ACCOUNT_CHANGE_EMAIL_CONFIRM: defaultAccountChangeEmailConfirmationMjmlTemplate,
ACCOUNT_DELETE: defaultAccountDeleteMjmlTemplate,
}; };
export const defaultMjmlSubjectTemplates: Record<MessageEventTypes, string> = { export const defaultMjmlSubjectTemplates: Record<MessageEventTypes, string> = {
@ -194,4 +278,9 @@ export const defaultMjmlSubjectTemplates: Record<MessageEventTypes, string> = {
ORDER_FULLY_PAID: "Order {{ order.number }} has been fully paid", ORDER_FULLY_PAID: "Order {{ order.number }} has been fully paid",
ORDER_CANCELLED: "Order {{ order.number }} has been cancelled", ORDER_CANCELLED: "Order {{ order.number }} has been cancelled",
INVOICE_SENT: "New invoice has been created", INVOICE_SENT: "New invoice has been created",
ACCOUNT_CONFIRMATION: "Account activation",
ACCOUNT_PASSWORD_RESET: "Password reset request",
ACCOUNT_CHANGE_EMAIL_REQUEST: "Email change request",
ACCOUNT_CHANGE_EMAIL_CONFIRM: "Email change confirmation",
ACCOUNT_DELETE: "Account deletion",
}; };

View file

@ -4,7 +4,7 @@ import { messageEventTypes } from "../../event-handlers/message-event-types";
export const sendgridConfigurationEventObjectSchema = z.object({ export const sendgridConfigurationEventObjectSchema = z.object({
active: z.boolean(), active: z.boolean(),
eventType: z.enum(messageEventTypes), eventType: z.enum(messageEventTypes),
template: z.string().min(1), template: z.string(),
}); });
export const sendgridConfigurationBaseObjectSchema = z.object({ export const sendgridConfigurationBaseObjectSchema = z.object({

View file

@ -27,7 +27,7 @@ const useStyles = makeStyles((theme) => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 20, gap: 20,
maxWidth: 600, maxWidth: 700,
}, },
}; };
}); });

View file

@ -2,12 +2,12 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types"; import { AppManifest } from "@saleor/app-sdk/types";
import packageJson from "../../../package.json"; import packageJson from "../../../package.json";
import { invoiceSentWebhook } from "./webhooks/invoice-sent";
import { orderCancelledWebhook } from "./webhooks/order-cancelled";
import { orderConfirmedWebhook } from "./webhooks/order-confirmed";
import { orderCreatedWebhook } from "./webhooks/order-created"; import { orderCreatedWebhook } from "./webhooks/order-created";
import { orderFulfilledWebhook } from "./webhooks/order-fulfilled"; import { orderFulfilledWebhook } from "./webhooks/order-fulfilled";
import { orderConfirmedWebhook } from "./webhooks/order-confirmed";
import { orderCancelledWebhook } from "./webhooks/order-cancelled";
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid"; import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
import { invoiceSentWebhook } from "./webhooks/invoice-sent";
export default createManifestHandler({ export default createManifestHandler({
async manifestFactory(context) { async manifestFactory(context) {
@ -15,7 +15,7 @@ export default createManifestHandler({
name: "Emails & Messages", name: "Emails & Messages",
tokenTargetUrl: `${context.appBaseUrl}/api/register`, tokenTargetUrl: `${context.appBaseUrl}/api/register`,
appUrl: context.appBaseUrl, appUrl: context.appBaseUrl,
permissions: ["MANAGE_ORDERS"], permissions: ["MANAGE_ORDERS", "MANAGE_USERS"],
id: "saleor.app.emails-and-messages", id: "saleor.app.emails-and-messages",
version: packageJson.version, version: packageJson.version,
webhooks: [ webhooks: [

View file

@ -1,6 +1,10 @@
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../saleor-app"; import { saleorApp } from "../../saleor-app";
import { createClient } from "../../lib/create-graphql-client";
import { logger } from "../../lib/logger";
import { getBaseUrl } from "../../lib/get-base-url";
import { registerNotifyWebhook } from "../../lib/register-notify-webhook";
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN; const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
@ -21,4 +25,18 @@ export default createAppRegisterHandler({
return true; return true;
}, },
], ],
onAuthAplSaved: async (request, ctx) => {
// Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest
logger.debug("onAuthAplSaved executing");
const baseUrl = getBaseUrl(request.headers);
const client = createClient(ctx.authData.saleorApiUrl, async () =>
Promise.resolve({ token: ctx.authData.token })
);
await registerNotifyWebhook({
client: client,
baseUrl: baseUrl,
});
logger.debug("Webhook registered");
},
}); });

View file

@ -0,0 +1,121 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../../saleor-app";
import { logger as pinoLogger } from "../../../lib/logger";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
import { MessageEventTypes } from "../../../modules/event-handlers/message-event-types";
// Notify event handles multiple event types which are recognized based on payload field `notify_event`.
// Handler recognizes if event is one of the supported typed and sends appropriate message.
interface NotifySubscriptionPayload {
notify_event: string;
payload: NotifyEventPayload;
meta: Meta;
}
interface Meta {
issued_at: Date;
version: string;
issuing_principal: IssuingPrincipal;
}
interface IssuingPrincipal {
id: null | string;
type: null | string;
}
export interface NotifyEventPayload {
user: User;
recipient_email: string;
channel_slug: string;
domain: string;
site_name: string;
logo_url: string;
token?: string;
confirm_url?: string;
reset_url?: string;
delete_url?: string;
old_email?: string;
new_email?: string;
redirect_url?: string;
}
interface User {
id: string;
email: string;
first_name: string;
last_name: string;
is_staff: boolean;
is_active: boolean;
private_metadata: Record<string, string>;
metadata: Record<string, string>;
language_code: string;
}
export const notifyWebhook = new SaleorAsyncWebhook<NotifySubscriptionPayload>({
name: "notify",
webhookPath: "api/webhooks/notify",
asyncEvent: "NOTIFY_USER",
apl: saleorApp.apl,
query: "{}", // We are using the default payload instead of subscription
});
const handler: NextWebhookApiHandler<NotifySubscriptionPayload> = async (req, res, context) => {
const logger = pinoLogger.child({
webhook: notifyWebhook.name,
});
logger.debug("Webhook received");
const { payload, authData } = context;
const { channel_slug: channel, recipient_email: recipientEmail } = payload.payload;
if (!recipientEmail?.length) {
logger.error(`The email recipient has not been specified in the event payload.`);
return res
.status(200)
.json({ error: "Email recipient has not been specified in the event payload." });
}
// Notify webhook event groups multiple event types under the one webhook. We need to map it to events recognized by the App
const notifyEventMapping: Record<string, MessageEventTypes> = {
account_confirmation: "ACCOUNT_CONFIRMATION",
account_delete: "ACCOUNT_DELETE",
account_password_reset: "ACCOUNT_PASSWORD_RESET",
account_change_email_request: "ACCOUNT_CHANGE_EMAIL_REQUEST",
account_change_email_confirm: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
};
const event = notifyEventMapping[payload.notify_event];
if (!event) {
logger.error(`The type of received notify event (${payload.notify_event}) is not supported.`);
return res
.status(200)
.json({ error: "Email recipient has not been specified in the event payload." });
}
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({
authData,
channel,
client,
event,
payload: payload.payload,
recipientEmail,
});
return res.status(200).json({ message: "The event has been handled" });
};
export default notifyWebhook.createHandler(handler);
export const config = {
api: {
bodyParser: false,
},
};