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:
parent
a3636f73ef
commit
ede7a2e808
11 changed files with 479 additions and 86 deletions
4
apps/emails-and-messages/src/lib/get-base-url.ts
Normal file
4
apps/emails-and-messages/src/lib/get-base-url.ts
Normal 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}`;
|
||||||
|
};
|
39
apps/emails-and-messages/src/lib/register-notify-webhook.ts
Normal file
39
apps/emails-and-messages/src/lib/register-notify-webhook.ts
Normal 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();
|
||||||
|
};
|
|
@ -18,7 +18,7 @@ const useStyles = makeStyles((theme) => {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 20,
|
gap: 20,
|
||||||
maxWidth: 600,
|
maxWidth: 700,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
};
|
};
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -27,7 +27,7 @@ const useStyles = makeStyles((theme) => {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 20,
|
gap: 20,
|
||||||
maxWidth: 600,
|
maxWidth: 700,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
121
apps/emails-and-messages/src/pages/api/webhooks/notify.ts
Normal file
121
apps/emails-and-messages/src/pages/api/webhooks/notify.ts
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in a new issue