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 }, ];