Compare commits
2 Commits
fix/openap
...
fix/magic-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c39fdff2 | ||
|
|
a6aae0cea1 |
@@ -122,10 +122,10 @@ export default function SideNavigation({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="hidden h-[45px] items-center justify-between pb-3 md:flex">
|
<div className="hidden h-14 items-center justify-between pb-[18px] pt-1.5 md:flex">
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<Link href="/" className="block">
|
<Link href="/" className="block">
|
||||||
<h1 className="pl-2 text-[16px] font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
<h1 className="pl-2 text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||||
kan.bn
|
kan.bn
|
||||||
</h1>
|
</h1>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function BoardDropdown() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
<HiEllipsisHorizontal className="h-6 w-6 text-dark-900" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
|
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
|
||||||
<div className="mb-4 flex w-full flex-row pt-[18px]">
|
<div className="mb-4 flex w-full flex-row">
|
||||||
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
|
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
|
||||||
<ListSelector
|
<ListSelector
|
||||||
cardPublicId={cardId ?? ""}
|
cardPublicId={cardId ?? ""}
|
||||||
@@ -210,17 +210,6 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
}
|
}
|
||||||
}, [card, getModalState, clearModalState]);
|
}, [card, getModalState, clearModalState]);
|
||||||
|
|
||||||
// Auto-resize title textarea
|
|
||||||
useEffect(() => {
|
|
||||||
const titleTextarea = document.getElementById(
|
|
||||||
"title",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
if (titleTextarea) {
|
|
||||||
titleTextarea.style.height = "auto";
|
|
||||||
titleTextarea.style.height = `${titleTextarea.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}, [card]);
|
|
||||||
|
|
||||||
if (!cardId) return <></>;
|
if (!cardId) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,71 +217,47 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
<PageHead
|
<PageHead
|
||||||
title={t`${card?.title ?? t`Card`} | ${board?.name ?? t`Board`}`}
|
title={t`${card?.title ?? t`Card`} | ${board?.name ?? t`Board`}`}
|
||||||
/>
|
/>
|
||||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
<div className="flex h-full flex-1 flex-row overflow-hidden">
|
||||||
{/* Full-width top strip with board link and dropdown */}
|
|
||||||
<div className="flex w-full items-center justify-between border-b-[1px] border-light-300 bg-light-50 px-8 py-2 dark:border-dark-300 dark:bg-dark-50">
|
|
||||||
{!card && isLoading && (
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<div className="h-[1.5rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{card && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Link
|
|
||||||
className="whitespace-nowrapleading-[1.5rem] text-sm font-bold text-light-900 dark:text-dark-950"
|
|
||||||
href={`${isTemplate ? "/templates" : "/boards"}`}
|
|
||||||
>
|
|
||||||
{workspace.name}
|
|
||||||
</Link>
|
|
||||||
<IoChevronForwardSharp className="h-[10px] w-[10px] text-light-900 dark:text-dark-900" />
|
|
||||||
<Link
|
|
||||||
className="whitespace-nowrap text-sm font-bold leading-[1.5rem] text-light-900 dark:text-dark-950"
|
|
||||||
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`}
|
|
||||||
>
|
|
||||||
{board?.name}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Dropdown />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!card && !isLoading && (
|
|
||||||
<p className="block p-0 py-0 font-bold leading-[1.5rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1rem]">
|
|
||||||
{t`Card not found`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="scrollbar-thumb-rounded-[4px] scrollbar-track-rounded-[4px] w-full flex-1 overflow-y-auto scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 hover:scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300 dark:hover:scrollbar-thumb-dark-300">
|
<div className="scrollbar-thumb-rounded-[4px] scrollbar-track-rounded-[4px] w-full flex-1 overflow-y-auto scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 hover:scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300 dark:hover:scrollbar-thumb-dark-300">
|
||||||
<div className="p-auto mx-auto flex h-full w-full max-w-[800px] flex-col">
|
<div className="p-auto mx-auto flex h-full w-full max-w-[800px] flex-col">
|
||||||
<div className="p-6 md:p-8">
|
<div className="p-6 md:p-8">
|
||||||
<div className="mb-8 md:mt-4">
|
<div className="mb-8 flex w-full items-center justify-between md:mt-6">
|
||||||
{!card && isLoading && (
|
{!card && isLoading && (
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
|
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||||
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{card && (
|
{card && (
|
||||||
<form
|
<>
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
<Link
|
||||||
className="w-full space-y-6"
|
className="whitespace-nowrap font-bold leading-[2.3rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1.2rem]"
|
||||||
>
|
href={`${isTemplate ? "/templates" : "/boards"}/${board?.publicId}`}
|
||||||
<div>
|
>
|
||||||
<textarea
|
{board?.name}
|
||||||
id="title"
|
</Link>
|
||||||
{...register("title")}
|
<IoChevronForwardSharp
|
||||||
onBlur={handleSubmit(onSubmit)}
|
size={18}
|
||||||
rows={1}
|
className="mx-2 text-light-900 dark:text-dark-900"
|
||||||
className="block w-full resize-none overflow-hidden border-0 bg-transparent p-0 py-0 font-bold leading-relaxed text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
|
/>
|
||||||
onInput={(e) => {
|
<form
|
||||||
const target = e.target as HTMLTextAreaElement;
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
target.style.height = "auto";
|
className="w-full space-y-6"
|
||||||
target.style.height = `${target.scrollHeight}px`;
|
>
|
||||||
}}
|
<div>
|
||||||
/>
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
{...register("title")}
|
||||||
|
onBlur={handleSubmit(onSubmit)}
|
||||||
|
className="block w-full border-0 bg-transparent p-0 py-0 font-bold tracking-tight text-neutral-900 focus:ring-0 dark:text-dark-1000 sm:text-[1.2rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="flex">
|
||||||
|
<Dropdown />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</>
|
||||||
)}
|
)}
|
||||||
{!card && !isLoading && (
|
{!card && !isLoading && (
|
||||||
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ services:
|
|||||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
- SMTP_SECURE=${SMTP_SECURE}
|
- SMTP_SECURE=${SMTP_SECURE}
|
||||||
- EMAIL_FROM=${EMAIL_FROM}
|
- EMAIL_FROM=${EMAIL_FROM}
|
||||||
|
- PLUNK_API_KEY=${PLUNK_API_KEY}
|
||||||
# Notifications
|
- PLUNK_API_URL=${PLUNK_API_URL}
|
||||||
- NOVU_API_KEY=${NOVU_API_KEY}
|
|
||||||
- DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
|
|
||||||
|
|
||||||
# S3 storage
|
# S3 storage
|
||||||
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
openapi: {
|
openapi: {
|
||||||
summary: "Get invite information by code",
|
summary: "Get invite information by code",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/invites/{inviteCode}",
|
path: "/workspaces/{workspacePublicId}/invites/{inviteCode}",
|
||||||
description: "Get invite information by invite code",
|
description: "Get invite information by invite code",
|
||||||
tags: ["Invites"],
|
tags: ["Invites"],
|
||||||
protect: false,
|
protect: false,
|
||||||
@@ -521,7 +521,7 @@ export const memberRouter = createTRPCRouter({
|
|||||||
openapi: {
|
openapi: {
|
||||||
summary: "Accept an invite link",
|
summary: "Accept an invite link",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/invites/accept",
|
path: "/workspaces/{workspacePublicId}/invites/accept",
|
||||||
description: "Accepts an invitation via invite link",
|
description: "Accepts an invitation via invite link",
|
||||||
tags: ["Invites"],
|
tags: ["Invites"],
|
||||||
protect: false,
|
protect: false,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { Subscription } from "@better-auth/stripe";
|
|
||||||
import type Stripe from "stripe";
|
|
||||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||||
import { stripe } from "@better-auth/stripe";
|
import { stripe } from "@better-auth/stripe";
|
||||||
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
|
|
||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
||||||
@@ -17,7 +14,7 @@ import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
|||||||
import * as userRepo from "@kan/db/repository/user.repo";
|
import * as userRepo from "@kan/db/repository/user.repo";
|
||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
import * as schema from "@kan/db/schema";
|
import * as schema from "@kan/db/schema";
|
||||||
import { notificationClient, sendEmail } from "@kan/email";
|
import { cloudMailerClient, sendEmail } from "@kan/email";
|
||||||
import { createStripeClient } from "@kan/stripe";
|
import { createStripeClient } from "@kan/stripe";
|
||||||
|
|
||||||
export const configuredProviders = socialProviderList.reduce<
|
export const configuredProviders = socialProviderList.reduce<
|
||||||
@@ -181,17 +178,19 @@ export const initAuth = (db: dbClient) => {
|
|||||||
freeTrial: {
|
freeTrial: {
|
||||||
days: 14,
|
days: 14,
|
||||||
onTrialStart: async (subscription) => {
|
onTrialStart: async (subscription) => {
|
||||||
await triggerWorkflow(db, "trial-start", subscription);
|
// Called when a trial starts
|
||||||
|
// @todo: send trial start email
|
||||||
|
// await sendTrialStartEmail(subscription.referenceId);
|
||||||
},
|
},
|
||||||
onTrialEnd: async ({ subscription }) => {
|
onTrialEnd: async ({ subscription }, request) => {
|
||||||
await triggerWorkflow(db, "trial-end", subscription);
|
// Called when a trial ends
|
||||||
|
// @todo: send trial end email
|
||||||
|
// await sendTrialEndEmail(user.email);
|
||||||
},
|
},
|
||||||
onTrialExpired: async (subscription) => {
|
onTrialExpired: async (subscription) => {
|
||||||
await triggerWorkflow(
|
// Called when a trial expires without conversion
|
||||||
db,
|
// @todo: send trial expired email
|
||||||
"trial-expired",
|
// await sendTrialExpiredEmail(subscription.referenceId);
|
||||||
subscription,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -203,17 +202,19 @@ export const initAuth = (db: dbClient) => {
|
|||||||
freeTrial: {
|
freeTrial: {
|
||||||
days: 14,
|
days: 14,
|
||||||
onTrialStart: async (subscription) => {
|
onTrialStart: async (subscription) => {
|
||||||
await triggerWorkflow(db, "trial-start", subscription);
|
// Called when a trial starts
|
||||||
|
// @todo: send trial start email
|
||||||
|
// await sendTrialStartEmail(subscription.referenceId);
|
||||||
},
|
},
|
||||||
onTrialEnd: async ({ subscription }) => {
|
onTrialEnd: async ({ subscription }, request) => {
|
||||||
await triggerWorkflow(db, "trial-end", subscription);
|
// Called when a trial ends
|
||||||
|
// @todo: send trial end email
|
||||||
|
// await sendTrialEndEmail(user.email);
|
||||||
},
|
},
|
||||||
onTrialExpired: async (subscription) => {
|
onTrialExpired: async (subscription) => {
|
||||||
await triggerWorkflow(
|
// Called when a trial expires without conversion
|
||||||
db,
|
// @todo: send trial expired email
|
||||||
"trial-expired",
|
// await sendTrialExpiredEmail(subscription.referenceId);
|
||||||
subscription,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -347,7 +348,17 @@ export const initAuth = (db: dbClient) => {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
async after(user) {
|
async after(user) {
|
||||||
let avatarKey = user.image;
|
if (cloudMailerClient) {
|
||||||
|
await cloudMailerClient.events.track({
|
||||||
|
event: "user-signup",
|
||||||
|
email: user.email,
|
||||||
|
data: {
|
||||||
|
name: user.name,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user.image &&
|
user.image &&
|
||||||
!user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!)
|
!user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!)
|
||||||
@@ -380,9 +391,6 @@ export const initAuth = (db: dbClient) => {
|
|||||||
ACL: "public-read",
|
ACL: "public-read",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
avatarKey = key;
|
|
||||||
|
|
||||||
await userRepo.update(db, user.id, {
|
await userRepo.update(db, user.id, {
|
||||||
image: key,
|
image: key,
|
||||||
});
|
});
|
||||||
@@ -390,51 +398,6 @@ export const initAuth = (db: dbClient) => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notificationClient) {
|
|
||||||
try {
|
|
||||||
const [firstName, ...rest] = user.name
|
|
||||||
.split(" ")
|
|
||||||
.filter(Boolean);
|
|
||||||
const lastName = rest.length ? rest.join(" ") : undefined;
|
|
||||||
const avatarUrl = avatarKey
|
|
||||||
? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await notificationClient.trigger({
|
|
||||||
to: {
|
|
||||||
subscriberId: user.id,
|
|
||||||
firstName: firstName,
|
|
||||||
lastName: lastName,
|
|
||||||
email: user.email,
|
|
||||||
avatar: avatarUrl,
|
|
||||||
data: {
|
|
||||||
emailVerified: user.emailVerified,
|
|
||||||
stripeCustomerId: user.stripeCustomerId,
|
|
||||||
createdAt: user.createdAt,
|
|
||||||
updatedAt: user.updatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workflowId: "user-signup",
|
|
||||||
});
|
|
||||||
|
|
||||||
await notificationClient.subscribers.credentials.update(
|
|
||||||
{
|
|
||||||
providerId: ChatOrPushProviderEnum.Discord,
|
|
||||||
credentials: {
|
|
||||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
|
|
||||||
},
|
|
||||||
integrationIdentifier: "discord",
|
|
||||||
},
|
|
||||||
user.id,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Error adding user to notification client",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -472,34 +435,3 @@ export const initAuth = (db: dbClient) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async function triggerWorkflow(
|
|
||||||
db: dbClient,
|
|
||||||
workflowId: string,
|
|
||||||
subscription: Subscription,
|
|
||||||
cancellationDetails?: Stripe.Subscription.CancellationDetails | null,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (!subscription.stripeCustomerId || !notificationClient) return;
|
|
||||||
|
|
||||||
const user = await userRepo.getByStripeCustomerId(
|
|
||||||
db,
|
|
||||||
subscription.stripeCustomerId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user || !notificationClient) return;
|
|
||||||
|
|
||||||
await notificationClient.trigger({
|
|
||||||
to: {
|
|
||||||
subscriberId: user.id,
|
|
||||||
},
|
|
||||||
payload: {
|
|
||||||
...subscription,
|
|
||||||
cancellationDetails,
|
|
||||||
},
|
|
||||||
workflowId,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error triggering workflow", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,15 +28,6 @@ export const getById = async (db: dbClient, userId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getByStripeCustomerId = async (
|
|
||||||
db: dbClient,
|
|
||||||
stripeCustomerId: string,
|
|
||||||
) => {
|
|
||||||
return await db.query.users.findFirst({
|
|
||||||
where: eq(users.stripeCustomerId, stripeCustomerId),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getByEmail = (db: dbClient, email: string) => {
|
export const getByEmail = (db: dbClient, email: string) => {
|
||||||
return db.query.users.findFirst({
|
return db.query.users.findFirst({
|
||||||
columns: {
|
columns: {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@novu/api": "^3.11.0",
|
"@plunk/node": "^3.0.3",
|
||||||
"@react-email/components": "^0.0.25",
|
"@react-email/components": "^0.0.25",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"react-email": "^3.0.1"
|
"react-email": "^3.0.1"
|
||||||
|
|||||||
10
packages/email/src/cloudMailerClient.tsx
Normal file
10
packages/email/src/cloudMailerClient.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Plunk from "@plunk/node";
|
||||||
|
|
||||||
|
export const cloudMailerClient =
|
||||||
|
process.env.NEXT_PUBLIC_KAN_ENV === "cloud" &&
|
||||||
|
process.env.PLUNK_API_KEY &&
|
||||||
|
process.env.PLUNK_API_URL
|
||||||
|
? new Plunk(process.env.PLUNK_API_KEY ?? "", {
|
||||||
|
baseUrl: process.env.PLUNK_API_URL,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const name = "email";
|
export const name = "email";
|
||||||
|
|
||||||
export { sendEmail } from "./sendEmail";
|
export { sendEmail } from "./sendEmail";
|
||||||
export { notificationClient } from "./notificationClient";
|
export { cloudMailerClient } from "./cloudMailerClient";
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Novu } from "@novu/api";
|
|
||||||
|
|
||||||
export const notificationClient =
|
|
||||||
process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && process.env.NOVU_API_KEY
|
|
||||||
? new Novu({ secretKey: process.env.NOVU_API_KEY })
|
|
||||||
: null;
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { render } from "@react-email/render";
|
import { render } from "@react-email/render";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
import { cloudMailerClient } from "./cloudMailerClient";
|
||||||
import JoinWorkspaceTemplate from "./templates/join-workspace";
|
import JoinWorkspaceTemplate from "./templates/join-workspace";
|
||||||
import MagicLinkTemplate from "./templates/magic-link";
|
import MagicLinkTemplate from "./templates/magic-link";
|
||||||
import ResetPasswordTemplate from "./templates/reset-password";
|
import ResetPasswordTemplate from "./templates/reset-password";
|
||||||
@@ -44,6 +45,15 @@ export const sendEmail = async (
|
|||||||
html,
|
html,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if (cloudMailerClient) {
|
||||||
|
// const response = await cloudMailerClient.emails.send({
|
||||||
|
// ...options,
|
||||||
|
// body: html,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return response;
|
||||||
|
// }
|
||||||
|
|
||||||
const response = await transporter.sendMail(options);
|
const response = await transporter.sendMail(options);
|
||||||
|
|
||||||
if (!response.accepted.length) {
|
if (!response.accepted.length) {
|
||||||
|
|||||||
@@ -71,13 +71,4 @@ export const updateSubscriptionSeats = async (
|
|||||||
return updatedSubscription;
|
return updatedSubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCancellationDetails = async (
|
|
||||||
stripeSubscriptionId: string,
|
|
||||||
): Promise<Stripe.Subscription.CancellationDetails | null> => {
|
|
||||||
const stripe = createStripeClient();
|
|
||||||
const stripeSubscription =
|
|
||||||
await stripe.subscriptions.retrieve(stripeSubscriptionId);
|
|
||||||
return stripeSubscription.cancellation_details ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { createStripeClient };
|
export { createStripeClient };
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -440,9 +440,9 @@ importers:
|
|||||||
|
|
||||||
packages/email:
|
packages/email:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@novu/api':
|
'@plunk/node':
|
||||||
specifier: ^3.11.0
|
specifier: ^3.0.3
|
||||||
version: 3.11.0
|
version: 3.0.3
|
||||||
'@react-email/components':
|
'@react-email/components':
|
||||||
specifier: ^0.0.25
|
specifier: ^0.0.25
|
||||||
version: 0.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 0.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -2683,9 +2683,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
'@novu/api@3.11.0':
|
|
||||||
resolution: {integrity: sha512-8u0mB5VThL7MhdxoN0UoA4CS9eu2k3Xa6iulauMhCHENuJNxUNuirJWq5t8jDoH4bFTQTfMN0VRkC0qodKR2qA==}
|
|
||||||
|
|
||||||
'@octokit/auth-token@3.0.4':
|
'@octokit/auth-token@3.0.4':
|
||||||
resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==}
|
resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -2765,6 +2762,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@plunk/node@3.0.3':
|
||||||
|
resolution: {integrity: sha512-f3NT418/EdLjebQ4arAwR64cnz7d5H7i1CeKh8+B0cVBrwzHvF+Q+QkhutF+FAp49yuI/BYc71qQH+RFc2fxxQ==}
|
||||||
|
|
||||||
'@popperjs/core@2.11.8':
|
'@popperjs/core@2.11.8':
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
|
||||||
@@ -10226,10 +10226,6 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.19.1
|
fastq: 1.19.1
|
||||||
|
|
||||||
'@novu/api@3.11.0':
|
|
||||||
dependencies:
|
|
||||||
zod: 3.25.76
|
|
||||||
|
|
||||||
'@octokit/auth-token@3.0.4': {}
|
'@octokit/auth-token@3.0.4': {}
|
||||||
|
|
||||||
'@octokit/core@4.2.4':
|
'@octokit/core@4.2.4':
|
||||||
@@ -10349,6 +10345,10 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@plunk/node@3.0.3':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@posthog/core@1.0.2': {}
|
'@posthog/core@1.0.2': {}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@
|
|||||||
"NEXT_PUBLIC_KAN_ENV",
|
"NEXT_PUBLIC_KAN_ENV",
|
||||||
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
|
"NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY",
|
||||||
"STRIPE_SECRET_KEY",
|
"STRIPE_SECRET_KEY",
|
||||||
"DISCORD_WEBHOOK_URL",
|
|
||||||
"STRIPE_WEBHOOK_SECRET",
|
"STRIPE_WEBHOOK_SECRET",
|
||||||
"STRIPE_WEBHOOK_SECRET_LEGACY",
|
"STRIPE_WEBHOOK_SECRET_LEGACY",
|
||||||
"STRIPE_PRO_PLAN_MONTHLY_PRICE_ID",
|
"STRIPE_PRO_PLAN_MONTHLY_PRICE_ID",
|
||||||
@@ -121,7 +120,8 @@
|
|||||||
"PORT",
|
"PORT",
|
||||||
"BETTER_AUTH_SECRET",
|
"BETTER_AUTH_SECRET",
|
||||||
"BETTER_AUTH_TRUSTED_ORIGINS",
|
"BETTER_AUTH_TRUSTED_ORIGINS",
|
||||||
"NOVU_API_KEY"
|
"PLUNK_API_KEY",
|
||||||
|
"PLUNK_API_URL"
|
||||||
],
|
],
|
||||||
"globalPassThroughEnv": [
|
"globalPassThroughEnv": [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
|||||||
Reference in New Issue
Block a user