Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { deleteWorkspaceFolders } from "@/lib/api/folders/delete-workspace-folders";
import { tokenCache } from "@/lib/auth/token-cache";
import { isBlacklistedEmail } from "@/lib/edge-config/is-blacklisted-email";
import { recordLink } from "@/lib/tinybird";
import { redis } from "@/lib/upstash";
import { webhookCache } from "@/lib/webhook/cache";
Expand Down Expand Up @@ -73,6 +74,10 @@ export async function customerSubscriptionDeleted(event: Stripe.Event) {
const workspaceLinks = workspace.links;
const workspaceUsers = workspace.users.map(({ user }) => user);

const isBlacklistedCancellation = await isBlacklistedEmail(
workspaceUsers.filter(({ email }) => email).map(({ email }) => email!),
);

const pipeline = redis.pipeline();
// remove root domain redirect for all domains from Redis
workspaceLinks.forEach(({ id, domain }) => {
Expand Down Expand Up @@ -155,15 +160,22 @@ export async function customerSubscriptionDeleted(event: Stripe.Event) {
url: "",
})),
),
// Log the deletion
log({
message:
":cry: Workspace *`" + workspace.slug + "`* deleted their subscription",
":cry: Workspace *`" +
workspace.slug +
"`* deleted their subscription" +
(isBlacklistedCancellation ? " (blacklisted / banned)" : ""),
type: "cron",
mention: true,
}),
sendCancellationFeedback({
owners: workspaceUsers,
}),

// Don't send feedback if the user was blacklisted / banned
!isBlacklistedCancellation &&
sendCancellationFeedback({
owners: workspaceUsers,
}),

// Disable the webhooks
prisma.webhook.updateMany({
Expand Down
20 changes: 15 additions & 5 deletions apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,21 @@ export const GET = withSession(async ({ session, params }) => {
});
}

const customer = invoice.workspace.stripeId
? ((await stripe.customers.retrieve(invoice.workspace.stripeId, {
expand: ["tax_ids"],
})) as Stripe.Customer | null)
: null;
let customer: Stripe.Customer | null = null;

if (invoice.workspace.stripeId) {
try {
const response = await stripe.customers.retrieve(
invoice.workspace.stripeId,
{
expand: ["tax_ids"],
},
);
customer = response as Stripe.Customer;
} catch (error) {
console.error(error);
}
}

const { amount: chargeAmount, currency: chargeCurrency } =
invoice.stripeChargeMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {
<Text style={tw("text-sm text-neutral-500 leading-6")}>
San Francisco, CA 94114
</Text>
<Text style={tw("text-sm text-neutral-500 leading-6")}>
Tax ID: US EIN {process.env.DUB_EIN}
</Text>
<Text style={tw("text-sm text-neutral-800 leading-6")}>
Invoice Number: <Text style={tw("font-bold")}>{payout.id}</Text>
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { cn } from "@dub/utils/src";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import { useAction } from "next-safe-action/hooks";
import { Dispatch, memo, SetStateAction, useState } from "react";
import { Dispatch, SetStateAction, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
Expand Down Expand Up @@ -378,7 +378,7 @@ function EmailPreview() {
>
<div className="mt-2 overflow-hidden rounded-md border border-neutral-200 bg-white">
<div className="grid grid-cols-1 gap-4 p-6 pb-10">
<MemoBlurImage
<BlurImage
src={program?.logo || "https://assets.dub.co/logo.png"}
alt={program?.name || "Dub"}
className="my-2 size-8 rounded-full"
Expand Down Expand Up @@ -413,8 +413,6 @@ function EmailPreview() {
);
}

const MemoBlurImage = memo(BlurImage);

export function InvitePartnerSheet({
isOpen,
...rest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import {
} from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { memo, useState } from "react";
import { useState } from "react";
import { toast } from "sonner";

const integrationSettings = {
Expand Down Expand Up @@ -278,7 +278,7 @@ export default function IntegrationPageClient({
<CarouselContent>
{integration.screenshots.map((src, idx) => (
<CarouselItem key={idx}>
<BlurImageMemo
<BlurImage
src={src}
alt={`Screenshot ${idx + 1} of ${integration.name}`}
width={900}
Expand Down Expand Up @@ -310,7 +310,7 @@ export default function IntegrationPageClient({
)
}
>
<BlurImageMemo
<BlurImage
src={src}
alt={`Screenshot ${idx + 1} thumbnail`}
width={900}
Expand All @@ -335,5 +335,3 @@ export default function IntegrationPageClient({
</MaxWidthWrapper>
);
}

const BlurImageMemo = memo(BlurImage);
36 changes: 21 additions & 15 deletions apps/web/lib/api/domains/claim-dot-link-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { DubApiError } from "@/lib/api/errors";
import { createLink } from "@/lib/api/links";
import { registerDomain } from "@/lib/dynadot/register-domain";
import { WorkspaceWithUsers } from "@/lib/types";
import { sendEmail } from "@dub/email";
import { resend } from "@dub/email/resend";
import { VARIANT_TO_FROM_MAP } from "@dub/email/resend/constants";
import DomainClaimed from "@dub/email/templates/domain-claimed";
import { prisma } from "@dub/prisma";
import { DEFAULT_LINK_PROPS } from "@dub/utils";
Expand Down Expand Up @@ -151,7 +152,7 @@ export async function claimDotLinkDomain({
return response;
}

const sendDomainClaimedEmails = async ({
export const sendDomainClaimedEmails = async ({
workspace,
domain,
}: {
Expand All @@ -174,17 +175,22 @@ const sendDomainClaimedEmails = async ({
},
});

workspaceWithOwner.users.map(({ user }) => {
if (user.email) {
sendEmail({
email: user.email,
subject: "Successfully claimed your .link domain!",
react: DomainClaimed({
email: user.email,
domain,
workspaceSlug: workspace.slug,
}),
});
}
});
const emails = workspaceWithOwner.users
.filter(({ user }) => user.email)
.map(({ user }) => ({
from: VARIANT_TO_FROM_MAP.notifications,
to: user.email!,
subject: "Successfully claimed your .link domain!",
react: DomainClaimed({
email: user.email!,
domain,
workspaceSlug: workspace.slug,
}),
}));

if (emails.length > 0) {
return await resend?.batch.send(emails);
}

return null;
};
9 changes: 8 additions & 1 deletion apps/web/lib/edge-config/is-blacklisted-email.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { get } from "@vercel/edge-config";

export const isBlacklistedEmail = async (email: string) => {
export const isBlacklistedEmail = async (email: string | string[]) => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
return false;
}
Expand All @@ -12,5 +12,12 @@ export const isBlacklistedEmail = async (email: string) => {
blacklistedEmails = [];
}
if (blacklistedEmails.length === 0) return false;

if (Array.isArray(email)) {
return email.some((e) =>
new RegExp(blacklistedEmails.join("|"), "i").test(e),
);
}

return new RegExp(blacklistedEmails.join("|"), "i").test(email);
};
4 changes: 1 addition & 3 deletions apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ module.exports = withAxiom({
"@dub/utils",
"@team-plain/typescript-sdk",
],
...(process.env.NODE_ENV === "production" && {
esmExternals: "loose",
}),
esmExternals: "loose",
},
webpack: (config, { webpack, isServer }) => {
if (isServer) {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"dev": "concurrently --kill-others \"pnpm prisma:generate && next dev --turbo --port 8888\" \"pnpm prisma:generate && pnpm prisma:studio --browser none\"",
"dev": "concurrently --kill-others \"pnpm prisma:generate && next dev --port 8888\" \"pnpm prisma:generate && pnpm prisma:studio --browser none\"",
"build": "pnpm prisma:generate && next build",
"lint": "next lint",
"start": "next start",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { Hyperlink } from "./icons/hyperlink";
import { LinesY } from "./icons/lines-y";
import { User } from "./icons/user";
import { SidebarNav, SidebarNavAreas, SidebarNavGroups } from "./sidebar-nav";
import { SidebarUsage } from "./usage";
import { SidebarUsage } from "./sidebar-usage";
import { useProgramApplicationsCount } from "./use-program-applications-count";
import { WorkspaceDropdown } from "./workspace-dropdown";

Expand Down
14 changes: 5 additions & 9 deletions apps/web/ui/layout/sidebar/partner-program-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,13 @@ import { ChevronsUpDown } from "lucide-react";
import Link from "next/link";
import { useParams, usePathname, useRouter } from "next/navigation";
import {
memo,
PropsWithChildren,
useCallback,
useMemo,
useRef,
useState,
} from "react";

// Helps prevent flickering from re-rendering. We might be able to just add this to `BlurImage` itself in the future.
const MemoBlurImage = memo(BlurImage);

export function PartnerProgramDropdown() {
const { programSlug } = useParams() as { programSlug?: string };

Expand Down Expand Up @@ -108,7 +104,7 @@ export function PartnerProgramDropdown() {
>
<div className="flex min-w-0 items-center gap-x-2.5 pr-2">
{selectedProgram?.logo && (
<MemoBlurImage
<BlurImage
src={selectedProgram.logo}
referrerPolicy="no-referrer"
width={40}
Expand All @@ -134,7 +130,7 @@ export function PartnerProgramDropdown() {
function PartnerDropdownPlaceholder() {
return (
<div className="flex w-full animate-pulse items-center gap-x-2.5 rounded-lg px-2 py-1.5">
<div className="size-5 animate-pulse rounded-full bg-neutral-200" />
<div className="size-6 animate-pulse rounded-full bg-neutral-200" />
<div className="h-7 w-28 grow animate-pulse rounded-md bg-neutral-200" />
<ChevronsUpDown className="h-4 w-4 text-neutral-400" aria-hidden="true" />
</div>
Expand Down Expand Up @@ -208,7 +204,7 @@ function ProgramList({
>
<div className="flex flex-col gap-0.5">
<Command.List>
{programs.map(({ slug, name, logo }) => (
{programs.map(({ id, slug, name, logo }) => (
<Command.Item
key={slug}
asChild
Expand All @@ -230,8 +226,8 @@ function ProgramList({
onClick={() => setOpenPopover(false)}
tabIndex={-1}
>
<MemoBlurImage
src={logo || `${OG_AVATAR_URL}${name}`}
<BlurImage
src={logo || `https://avatar.vercel.sh/${id}`}
width={40}
height={40}
alt={name}
Expand Down
8 changes: 4 additions & 4 deletions apps/web/ui/layout/sidebar/payout-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ export const PayoutStats = memo(() => {

return (
<AnimatedSizeContainer height>
<div className="border-border-default rounded-lg border p-2 pt-1">
<div className="border-border-default grid gap-3 border-t p-3">
<Link
className="group flex items-center justify-between gap-2 px-2 py-2"
className="group flex items-center justify-between gap-2"
href="/settings/payouts"
>
<div className="text-content-emphasis flex items-center gap-2 text-sm font-semibold">
<div className="text-content-default flex items-center gap-2 text-sm font-semibold">
<MoneyBills2 className="size-4" />
Payouts
</div>
<ChevronRight className="text-content-muted group-hover:text-content-default size-3 transition-[color,transform] group-hover:translate-x-0.5 [&_*]:stroke-2" />
</Link>

<div className="mt-2 flex flex-col gap-4 px-2">
<div className="flex flex-col gap-4">
<div className="grid gap-1 text-xs">
<p className="text-content-subtle font-medium">Upcoming payouts</p>
<div className="flex items-center gap-1">
Expand Down
6 changes: 3 additions & 3 deletions apps/web/ui/layout/sidebar/program-help-support.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ export const ProgramHelpSupport = memo(() => {
if (supportItems.length === 0) return null;

return (
<div className="border-border-default grid gap-1 rounded-lg border p-2">
<p className="text-content-emphasis text-balance px-2.5 py-2 text-sm font-semibold">
<div className="border-border-default grid gap-2 border-t p-3">
<div className="text-content-default px-2 text-sm font-semibold">
{program.name.length <= 12 ? `${program.name} ` : ""}
Program Support
</p>
</div>
<div className="grid grid-cols-1">
{supportItems.map(({ icon: Icon, label, href }) => (
<a
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@dub/ui",
"description": "UI components for Dub",
"version": "0.2.52",
"version": "0.2.53",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
7 changes: 4 additions & 3 deletions packages/ui/src/blur-image.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cn } from "@dub/utils";
import Image, { ImageProps } from "next/image";
import { useEffect, useState } from "react";
import { memo, useEffect, useState } from "react";

export function BlurImage(props: ImageProps) {
// Helps prevent flickering from re-rendering
export const BlurImage = memo((props: ImageProps) => {
const [loading, setLoading] = useState(true);
const [src, setSrc] = useState(props.src);
useEffect(() => setSrc(props.src), [props.src]); // update the `src` value when the `prop.src` value changes
Expand All @@ -28,4 +29,4 @@ export function BlurImage(props: ImageProps) {
unoptimized
/>
);
}
});