Skip to content

Commit

Permalink
Add subscription confirmation feature and enhance Pill component with…
Browse files Browse the repository at this point in the history
… tooltip support
  • Loading branch information
simlarsen committed Dec 17, 2024
1 parent f7e31a4 commit d670cca
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 203 deletions.
19 changes: 8 additions & 11 deletions Common/Models/DatabaseModels/StatusPageSubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,6 @@ export default class StatusPageSubscriber extends BaseModel {
})
public deletedByUserId?: ObjectID = undefined;


@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Expand All @@ -460,32 +459,30 @@ export default class StatusPageSubscriber extends BaseModel {
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Is Subscription Confirmed",
description: "Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)",
description:
"Has subscriber confirmed their subscription? (for example, by clicking on a confirmation link in an email)",
})
@Column({
type: ColumnType.Boolean,
default: false,
})
public isSubscriptionConfirmed?: boolean = undefined;


@ColumnAccessControl({
create: [
],
read: [
],
update: [
],
create: [],
read: [],
update: [],
})
@TableColumn({
isDefaultValueColumn: false,
type: TableColumnType.ShortText,
title: "Subscription Confirmation Token",
description: "Token used to confirm subscription. This is a random token that is sent to the subscriber's email address to confirm their subscription.",
description:
"Token used to confirm subscription. This is a random token that is sent to the subscriber's email address to confirm their subscription.",
})
@Column({
type: ColumnType.ShortText,
nullable: true
nullable: true,
})
public subscriptionConfirmationToken?: string = undefined;

Expand Down
9 changes: 5 additions & 4 deletions Common/Server/API/StatusPageAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export default class StatusPageAPI extends BaseAPI<
public constructor() {
super(StatusPage, StatusPageService);


// confirm subscription api
this.router.get(
`${new this.entityType()
Expand All @@ -102,7 +101,7 @@ export default class StatusPageAPI extends BaseAPI<
subscriptionConfirmationToken: token,
},
select: {
isSubscriptionConfirmed: true,
isSubscriptionConfirmed: true,
},
props: {
isRoot: true,
Expand All @@ -113,11 +112,13 @@ export default class StatusPageAPI extends BaseAPI<
return Response.sendErrorResponse(
req,
res,
new NotFoundException("Subscriber not found or confirmation token is invalid"),
new NotFoundException(
"Subscriber not found or confirmation token is invalid",
),
);
}

// check if subscription confirmed already.
// check if subscription confirmed already.

if (subscriber.isSubscriptionConfirmed) {
return Response.sendEmptySuccessResponse(req, res);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class MigrationName1734435866602 implements MigrationInterface {
public name = 'MigrationName1734435866602'
public name = "MigrationName1734435866602";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" ADD "isSubscriptionConfirmed" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" ADD "subscriptionConfirmationToken" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "subscriptionConfirmationToken"`);
await queryRunner.query(`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isSubscriptionConfirmed"`);
}
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "isSubscriptionConfirmed" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "subscriptionConfirmationToken" character varying`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "subscriptionConfirmationToken"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "isSubscriptionConfirmed"`,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,5 @@ export default [
MigrationName1731435267537,
MigrationName1731435514287,
MigrationName1732553444010,
MigrationName1734435866602
MigrationName1734435866602,
];
75 changes: 34 additions & 41 deletions Common/Server/Services/StatusPageSubscriberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,21 @@ export class Service extends DatabaseService<Model> {

data.data.projectId = statuspage.projectId;

const isEmailSubscriber: boolean = !!data.data.subscriberEmail;
const isSubscriptionConfirmed: boolean = !!data.data.isSubscriptionConfirmed;
const isEmailSubscriber: boolean = Boolean(data.data.subscriberEmail);
const isSubscriptionConfirmed: boolean = Boolean(
data.data.isSubscriptionConfirmed,
);

if (isEmailSubscriber && !isSubscriptionConfirmed) {
data.data.isSubscriptionConfirmed = false;
}else{
} else {
data.data.isSubscriptionConfirmed = true; // if the subscriber is not email, then set it to true for SMS subscribers.
}

data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(100000, 999999).toString();
data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(
100000,
999999,
).toString();

return { createBy: data, carryForward: statuspage };
}
Expand All @@ -192,14 +197,11 @@ export class Service extends DatabaseService<Model> {
onCreate.carryForward.name ||
"Status Page";


const unsubscribeLink: string = this.getUnsubscribeLink(
URL.fromString(statusPageURL),
createdItem.id!,
).toString();



if (
createdItem.statusPageId &&
createdItem.subscriberPhone &&
Expand Down Expand Up @@ -250,23 +252,22 @@ export class Service extends DatabaseService<Model> {
createdItem.subscriberEmail &&
createdItem._id
) {

// Call mail service and send an email.

// get status page domain for this status page.
// if the domain is not found, use the internal status page preview link.

const isSubcriptionConfirmed: boolean = !!createdItem.isSubscriptionConfirmed;
const isSubcriptionConfirmed: boolean = Boolean(
createdItem.isSubscriptionConfirmed,
);

if (!isSubcriptionConfirmed) {

await this.sendConfirmSubscriptionEmail({
subscriberId: createdItem.id!,
});
}

if (isSubcriptionConfirmed && createdItem.sendYouHaveSubscribedMessage) {

await this.sendYouHaveSubscribedEmail({
subscriberId: createdItem.id!,
});
Expand All @@ -276,7 +277,6 @@ export class Service extends DatabaseService<Model> {
return createdItem;
}


public async sendConfirmSubscriptionEmail(data: {
subscriberId: ObjectID;
}): Promise<void> {
Expand Down Expand Up @@ -338,20 +338,19 @@ export class Service extends DatabaseService<Model> {
statusPage.id,
);

const statusPageName: string = statusPage.pageTitle || statusPage.name || "Status Page";
const statusPageName: string =
statusPage.pageTitle || statusPage.name || "Status Page";

const host: Hostname = await DatabaseConfig.getHost();

const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink(
{
statusPageUrl: statusPageURL,
confirmationToken: subscriber.subscriptionConfirmationToken || "",
statusPageSubscriberId: subscriber.id!,
statusPageId: subscriber.statusPageId,
}
).toString();
const confirmSubscriptionLink: string = this.getConfirmSubscriptionLink({
statusPageUrl: statusPageURL,
confirmationToken: subscriber.subscriptionConfirmationToken || "",
statusPageSubscriberId: subscriber.id!,
statusPageId: subscriber.statusPageId,
}).toString();

if (
subscriber.statusPageId &&
Expand All @@ -365,11 +364,10 @@ export class Service extends DatabaseService<Model> {
vars: {
statusPageName: statusPageName,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol
, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
statusPageUrl: statusPageURL,
isPublicStatusPage: statusPage.isPublicStatusPage
Expand All @@ -387,17 +385,13 @@ export class Service extends DatabaseService<Model> {
},
).catch((err: Error) => {
logger.error(err);
}
);
});
}
}


public async sendYouHaveSubscribedEmail(data: {
subscriberId: ObjectID;
}): Promise<void> {


// get subscriber
const subscriber: Model | null = await this.findOneBy({
query: {
Expand All @@ -421,7 +415,7 @@ export class Service extends DatabaseService<Model> {
return;
}

const statusPage: StatusPage | null = await StatusPageService.findOneBy({
const statusPage: StatusPage | null = await StatusPageService.findOneBy({
query: {
_id: subscriber.statusPageId.toString(),
},
Expand All @@ -447,16 +441,16 @@ export class Service extends DatabaseService<Model> {
},
});

if(!statusPage || !statusPage.id) {
if (!statusPage || !statusPage.id) {
return;
}


const statusPageURL: string = await StatusPageService.getStatusPageURL(
statusPage.id,
);

const statusPageName: string = statusPage.pageTitle ||statusPage.name || "Status Page";
const statusPageName: string =
statusPage.pageTitle || statusPage.name || "Status Page";

const host: Hostname = await DatabaseConfig.getHost();

Expand All @@ -480,9 +474,9 @@ export class Service extends DatabaseService<Model> {
statusPageName: statusPageName,
logoUrl: statusPage.logoFileId
? new URL(httpProtocol, host)
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
.addRoute(FileRoute)
.addRoute("/image/" + statusPage.logoFileId)
.toString()
: "",
statusPageUrl: statusPageURL,
isPublicStatusPage: statusPage.isPublicStatusPage
Expand All @@ -504,12 +498,11 @@ export class Service extends DatabaseService<Model> {
}
}


public getConfirmSubscriptionLink(data: {
statusPageUrl: string;
confirmationToken: string;
statusPageSubscriberId: ObjectID
statusPageId: ObjectID
statusPageSubscriberId: ObjectID;
statusPageId: ObjectID;
}): URL {
return URL.fromString(data.statusPageUrl).addRoute(
`/confirm-subscription/${data.statusPageId.toString()}/${data.statusPageSubscriberId.toString()}?token=${data.confirmationToken}`,
Expand Down
13 changes: 5 additions & 8 deletions Common/UI/Components/Pill/Pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Black } from "Common/Types/BrandColors";
import Color from "Common/Types/Color";
import React, { CSSProperties, FunctionComponent, ReactElement } from "react";
import Tooltip from "../Tooltip/Tooltip";
import { GetReactElementFunction } from "../../Types/FunctionTypes";

export enum PillSize {
Small = "10px",
Expand Down Expand Up @@ -42,7 +43,7 @@ const Pill: FunctionComponent<ComponentProps> = (
);
}

const getPillElement = (): ReactElement => {
const getPillElement: GetReactElementFunction = (): ReactElement => {
return (
<span
data-testid="pill"
Expand All @@ -69,14 +70,10 @@ const Pill: FunctionComponent<ComponentProps> = (
};

if (props.tooltip) {
return (
<Tooltip text={props.tooltip}>
{getPillElement()}
</Tooltip>
);
};
return <Tooltip text={props.tooltip}>{getPillElement()}</Tooltip>;
}

return getPillElement();
}
};

export default Pill;
15 changes: 11 additions & 4 deletions Dashboard/src/Pages/StatusPages/View/EmailSubscribers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
isSubscriptionConfirmed: true,
},
title: "Send Confirmation Email",
description: "Send a confirmation email to this subscriber with a link to confirm subscription.",
description:
"Send a confirmation email to this subscriber with a link to confirm subscription.",
fieldType: FormFieldSchemaType.Toggle,
required: false,
doNotShowWhenEditing: true,
Expand Down Expand Up @@ -313,10 +314,16 @@ const StatusPageDelete: FunctionComponent<PageComponentProps> = (
return <Pill color={Red} text={"Unsubscribed"} />;
}

if(!item["isSubscriptionConfirmed"]) {
return <Pill color={Yellow} text={"Awaiting Confirmation"} tooltip="Confirmation email sent to this user. Please click on the link to confirm subscription" />;
if (!item["isSubscriptionConfirmed"]) {
return (
<Pill
color={Yellow}
text={"Awaiting Confirmation"}
tooltip="Confirmation email sent to this user. Please click on the link to confirm subscription"
/>
);
}

return <Pill color={Green} text={"Subscribed"} />;
},
},
Expand Down
Loading

0 comments on commit d670cca

Please sign in to comment.