diff --git a/.changeset/weak-points-tan.md b/.changeset/weak-points-tan.md
new file mode 100644
index 00000000..9c0b1746
--- /dev/null
+++ b/.changeset/weak-points-tan.md
@@ -0,0 +1,6 @@
+---
+"@comet/brevo-admin": minor
+"@comet/brevo-api": minor
+---
+
+Add a brevo configuration field for `unsubscriptionPageId`
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index 81908efb..abecc077 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -508,6 +508,7 @@ type BrevoConfig implements DocumentInterface {
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
+ unsubscriptionPageId: String!
createdAt: DateTime!
scope: EmailCampaignContentScope!
}
@@ -1036,6 +1037,7 @@ input BrevoConfigInput {
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
+ unsubscriptionPageId: String!
}
input BrevoConfigUpdateInput {
@@ -1044,4 +1046,5 @@ input BrevoConfigUpdateInput {
doubleOptInTemplateId: Int
folderId: Int
allowedRedirectionUrl: String
+ unsubscriptionPageId: String
}
diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts
index 80b28b0e..73e7d5c6 100644
--- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts
+++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts
@@ -7,6 +7,7 @@ export const brevoConfigFormFragment = gql`
doubleOptInTemplateId
folderId
allowedRedirectionUrl
+ unsubscriptionPageId
}
`;
diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx
index 6804e50f..52383908 100644
--- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx
+++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx
@@ -52,6 +52,7 @@ type FormValues = {
doubleOptInTemplate: Option;
folderId: number;
allowedRedirectionUrl: string;
+ unsubscriptionPageId: string;
};
interface FormProps {
@@ -128,6 +129,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
: undefined,
allowedRedirectionUrl: data?.brevoConfig?.allowedRedirectionUrl ?? "",
folderId: data?.brevoConfig?.folderId ?? 1,
+ unsubscriptionPageId: data?.brevoConfig?.unsubscriptionPageId ?? "",
};
}, [
data?.brevoConfig?.folderId,
@@ -137,6 +139,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
data?.brevoConfig?.senderMail,
data?.brevoConfig?.senderName,
doubleOptInTemplatesData?.doubleOptInTemplates,
+ data?.brevoConfig?.unsubscriptionPageId,
sendersData?.senders,
]);
@@ -170,7 +173,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
const sender = sendersData?.senders?.find((s) => s.email === state.sender.value);
- if (!sender || !state.doubleOptInTemplate || !state.allowedRedirectionUrl) {
+ if (!sender || !state.doubleOptInTemplate || !state.allowedRedirectionUrl || !state.unsubscriptionPageId) {
throw new Error("Not all required fields are set");
}
@@ -180,6 +183,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
doubleOptInTemplateId: Number(state.doubleOptInTemplate.value),
folderId: state.folderId ?? 1,
allowedRedirectionUrl: state?.allowedRedirectionUrl ?? "",
+ unsubscriptionPageId: state.unsubscriptionPageId,
};
if (mode === "edit") {
@@ -215,6 +219,19 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
return ;
}
+ const validateUnsubscriptionPageId = (value: string) => {
+ const validUnsubscriptionPageId = /^[a-zA-Z0-9]{24}$/;
+ if (!validUnsubscriptionPageId.test(value)) {
+ return (
+
+ );
+ }
+ return undefined;
+ };
+
return (
apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}>
{({ values }) => {
@@ -303,6 +320,18 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement {
}
validate={validateUrl}
/>
+
+ }
+ validate={validateUnsubscriptionPageId}
+ />
>
);
diff --git a/packages/api/schema.gql b/packages/api/schema.gql
index d4e98867..7da0d8cc 100644
--- a/packages/api/schema.gql
+++ b/packages/api/schema.gql
@@ -210,6 +210,7 @@ type BrevoConfig implements DocumentInterface {
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
+ unsubscriptionPageId: String!
createdAt: DateTime!
scope: EmailCampaignContentScope!
}
@@ -401,6 +402,7 @@ input BrevoConfigInput {
doubleOptInTemplateId: Int!
folderId: Int!
allowedRedirectionUrl: String!
+ unsubscriptionPageId: String!
}
input BrevoConfigUpdateInput {
@@ -409,4 +411,5 @@ input BrevoConfigUpdateInput {
doubleOptInTemplateId: Int
folderId: Int
allowedRedirectionUrl: String
+ unsubscriptionPageId: String
}
diff --git a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts
index f1d8dab5..062f65ff 100644
--- a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts
+++ b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts
@@ -59,11 +59,13 @@ export class BrevoApiCampaignsService {
htmlContent,
sender,
scheduledAt,
+ unsubscriptionPageId,
}: {
campaign: EmailCampaignInterface;
htmlContent: string;
sender: { name: string; mail: string };
scheduledAt?: Date;
+ unsubscriptionPageId?: string;
}): Promise {
try {
const targetGroups = await campaign.targetGroups.loadItems();
@@ -75,6 +77,7 @@ export class BrevoApiCampaignsService {
recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
+ unsubscriptionPageId,
};
const data = await this.getCampaignsApi(campaign.scope).createEmailCampaign(emailCampaign);
diff --git a/packages/api/src/brevo-config/dto/brevo-config.input.ts b/packages/api/src/brevo-config/dto/brevo-config.input.ts
index 955dab4f..21af21cd 100644
--- a/packages/api/src/brevo-config/dto/brevo-config.input.ts
+++ b/packages/api/src/brevo-config/dto/brevo-config.input.ts
@@ -1,6 +1,6 @@
import { PartialType } from "@comet/cms-api";
import { Field, InputType, Int } from "@nestjs/graphql";
-import { IsEmail, IsInt, IsNotEmpty, IsString, IsUrl } from "class-validator";
+import { IsAlphanumeric, IsEmail, IsInt, IsNotEmpty, IsString, IsUrl, Length } from "class-validator";
@InputType()
export class BrevoConfigInput {
@@ -27,6 +27,12 @@ export class BrevoConfigInput {
@Field()
@IsUrl({ require_tld: process.env.NODE_ENV === "production" })
allowedRedirectionUrl: string;
+
+ @IsString()
+ @Field()
+ @Length(24)
+ @IsAlphanumeric()
+ unsubscriptionPageId: string;
}
@InputType()
diff --git a/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts
index c295388d..2c606bc2 100644
--- a/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts
+++ b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts
@@ -14,6 +14,7 @@ export interface BrevoConfigInterface {
doubleOptInTemplateId: number;
folderId: number;
allowedRedirectionUrl: string;
+ unsubscriptionPageId: string;
createdAt: Date;
updatedAt: Date;
scope: EmailCampaignScopeInterface;
@@ -52,6 +53,10 @@ export class BrevoConfigEntityFactory {
@Field()
allowedRedirectionUrl: string;
+ @Property({ columnType: "text" })
+ @Field(() => String)
+ unsubscriptionPageId: string;
+
@Property({
columnType: "timestamp with time zone",
})
diff --git a/packages/api/src/email-campaign/email-campaigns.service.ts b/packages/api/src/email-campaign/email-campaigns.service.ts
index d1d870ed..e6246f51 100644
--- a/packages/api/src/email-campaign/email-campaigns.service.ts
+++ b/packages/api/src/email-campaign/email-campaigns.service.ts
@@ -72,6 +72,7 @@ export class EmailCampaignsService {
htmlContent,
sender: { name: brevoConfig.senderName, mail: brevoConfig.senderMail },
scheduledAt,
+ unsubscriptionPageId: brevoConfig.unsubscriptionPageId,
});
wrap(campaign).assign({ brevoId });
diff --git a/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts b/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts
index 4dde972c..b008500d 100644
--- a/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts
+++ b/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts
@@ -22,6 +22,7 @@ export interface EmailCampaignInterface {
scope: EmailCampaignScopeInterface;
sendingState: SendingState;
targetGroups: Collection;
+ unsubscriptionPageId?: string;
}
export function createEmailCampaignEntity({
diff --git a/packages/api/src/mikro-orm/migrations/Migration20241024071748.ts b/packages/api/src/mikro-orm/migrations/Migration20241024071748.ts
new file mode 100644
index 00000000..e4c02ffd
--- /dev/null
+++ b/packages/api/src/mikro-orm/migrations/Migration20241024071748.ts
@@ -0,0 +1,7 @@
+import { Migration } from "@mikro-orm/migrations";
+
+export class Migration20241024071748 extends Migration {
+ async up(): Promise {
+ this.addSql('alter table "BrevoConfig" add column "unsubscriptionPageId" text not null;');
+ }
+}
diff --git a/packages/api/src/mikro-orm/migrations/migrations.ts b/packages/api/src/mikro-orm/migrations/migrations.ts
index ebb9a181..340da691 100644
--- a/packages/api/src/mikro-orm/migrations/migrations.ts
+++ b/packages/api/src/mikro-orm/migrations/migrations.ts
@@ -12,6 +12,7 @@ import { Migration20240830112400 } from "./Migration20240830112400";
import { Migration20241016123307 } from "./Migration20241016123307";
import { Migration20241018110515 } from "./Migration20241018110515";
import { Migration20241022144400 } from "./Migration20241022144400";
+import { Migration20241024071748 } from "./Migration20241024071748";
import { Migration20241119101706 } from "./Migration20241119101706";
export const migrationsList: MigrationObject[] = [
@@ -28,4 +29,5 @@ export const migrationsList: MigrationObject[] = [
{ name: "Migration20241018110515", class: Migration20241018110515 },
{ name: "Migration20241119101706", class: Migration20241119101706 },
{ name: "Migration20241022144400", class: Migration20241022144400 },
+ { name: "Migration20241024071748", class: Migration20241024071748 },
];