Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
1 change: 0 additions & 1 deletion apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ UNSPLASH_ACCESS_KEY=
STORAGE_ACCESS_KEY_ID=
STORAGE_SECRET_ACCESS_KEY=
STORAGE_ENDPOINT=
STORAGE_BASE_URL=

# Used for internal monitoring & paging
# You can remove this by removing `DUB_SLACK_HOOK_CRON` and `DUB_SLACK_HOOK_LINKS` from the codebase
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/api/track/visit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const POST = withAxiom(async (req: AxiomRequest) => {
workspaceId: cachedLink.projectId,
skipRatelimit: true,
...(referrer && { referrer }),
shouldPassClickId: true,
});
}
})(),
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DubApiError } from "@/lib/api/errors";
import { hashToken, withSession } from "@/lib/auth";
import { storage } from "@/lib/storage";
import { ratelimit, redis } from "@/lib/upstash";
import { base64ImageSchema } from "@/lib/zod/schemas/misc";
import { sendEmail } from "@dub/email";
import { unsubscribe } from "@dub/email/resend/unsubscribe";
import ConfirmEmailChange from "@dub/email/templates/confirm-email-change";
Expand All @@ -15,7 +16,7 @@ import { z } from "zod";
const updateUserSchema = z.object({
name: z.preprocess(trim, z.string().min(1).max(64)).optional(),
email: z.preprocess(trim, z.string().email()).optional(),
image: z.string().url().optional(),
image: base64ImageSchema.nullish(),
source: z.preprocess(trim, z.string().min(1).max(32)).optional(),
defaultWorkspace: z.preprocess(trim, z.string().min(1)).optional(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { toast } from "sonner";
export function Form() {
const router = useRouter();
const { isMobile } = useMediaQuery();
const [isUploading, setIsUploading] = useState(false);
const [hasSubmitted, setHasSubmitted] = useState(false);
const { activeWorkspaceDomains, loading } = useDomains();
const { id: workspaceId, slug: workspaceSlug, mutate } = useWorkspace();

Expand All @@ -35,8 +37,6 @@ export function Form() {
formState: { isSubmitting },
} = useFormContext<ProgramData>();

const [hasSubmitted, setHasSubmitted] = useState(false);

const { executeAsync, isPending } = useAction(onboardProgramAction, {
onSuccess: () => {
router.push(`/${workspaceSlug}/programs/new/rewards`);
Expand All @@ -59,8 +59,6 @@ export function Form() {
});
};

const [isUploading, setIsUploading] = useState(false);

// Handle logo upload
const handleUpload = async (file: File) => {
setIsUploading(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,25 @@ import Link from "next/link";
import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { toast } from "sonner";
import { mutate } from "swr";

export function PageClient() {
const {
getValues,
formState: { isSubmitting, isSubmitSuccessful },
} = useFormContext<ProgramData>();
const { id: workspaceId, slug: workspaceSlug, mutate } = useWorkspace();
const {
id: workspaceId,
slug: workspaceSlug,
mutate: mutateWorkspace,
} = useWorkspace();

const data = getValues();

const { executeAsync, isPending } = useAction(onboardProgramAction, {
onSuccess: async () => {
await mutate(`/api/programs?workspaceId=${workspaceId}`);
},
onError: ({ error }) => {
toast.error(error.serverError);
},
Expand All @@ -36,8 +44,6 @@ export function PageClient() {
workspaceId,
step: "create-program",
});

mutate();
};

const isValid = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,23 @@ export function Form() {
formState: { isSubmitting },
} = useFormContext<ProgramData>();

const [programType, rewardful, amount] = watch([
const [programType, rewardful, amount, maxDuration] = watch([
"programType",
"rewardful",
"amount",
"maxDuration",
]);

useEffect(() => {
if (programType === "new") {
setValue("rewardful", null);
} else if (programType === "import") {
setValue("type", null);
setValue("amount", null);
setValue("maxDuration", null);
}
}, [programType]);

const { executeAsync, isPending } = useAction(onboardProgramAction, {
onSuccess: () => {
router.push(`/${workspaceSlug}/programs/new/partners`);
Expand All @@ -94,30 +105,15 @@ export function Form() {
});

const onSubmit = async (data: ProgramData) => {
if (!workspaceId) return;

const programData = {
...data,
...(programType === "new" && {
rewardful: null,
apiKeyPrefix: null,
amount: data.amount ? data.amount * 100 : null,
}),
...(programType === "import" && {
type: null,
amount: null,
maxDuration: null,
}),
};
if (!workspaceId) {
return;
}

setHasSubmitted(true);

await executeAsync({
...programData,
...data,
workspaceId,
maxDuration:
Infinity === Number(programData.maxDuration)
? null
: programData.maxDuration,
step: "configure-reward",
});
};
Expand Down Expand Up @@ -216,10 +212,6 @@ const NewProgramForm = ({ register, watch, setValue }: FormProps) => {

useEffect(() => {
setCommissionStructure(maxDuration === 0 ? "one-off" : "recurring");

if (maxDuration === null) {
setValue("maxDuration", Infinity);
}
}, [maxDuration]);

return (
Expand Down Expand Up @@ -320,7 +312,7 @@ const NewProgramForm = ({ register, watch, setValue }: FormProps) => {

if (value === "recurring") {
setCommissionStructure("recurring");
setValue("maxDuration", 3, {
setValue("maxDuration", 12, {
shouldValidate: true,
});
}
Expand Down Expand Up @@ -351,7 +343,15 @@ const NewProgramForm = ({ register, watch, setValue }: FormProps) => {
Duration
</label>
<select
{...register("maxDuration", { valueAsNumber: true })}
{...register("maxDuration", {
setValueAs: (v) => {
if (v === "" || v === null) {
return null;
}

return parseInt(v);
},
})}
className="mt-2 block w-full rounded-md border border-neutral-300 bg-white py-2 pl-3 pr-10 text-sm text-neutral-900 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500"
>
{RECURRING_MAX_DURATIONS.filter((v) => v !== 0).map(
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/app.dub.co/(new-program)/form-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function FormWrapper({ children }: { children: React.ReactNode }) {
defaultRewardType: "sale",
type: "percentage",
amount: null,
maxDuration: 12,
partners: [{ email: "", key: "" }],
},
values: programOnboarding
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/app.dub.co/(new-program)/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export function ProgramOnboardingHeader() {
const { isMobile } = useMediaQuery();
const { getValues } = useFormContext();
const { isOpen, setIsOpen } = useSidebar();

const { id: workspaceId, slug: workspaceSlug } = useWorkspace();

useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/app.dub.co/embed/referrals/activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ export function ReferralsEmbedActivity({
</span>
<span className="text-content-default text-base font-medium leading-none">
{nFormatter(value, { full: true })}{" "}
{subValue && (
{subValue || subValue === 0 ? (
<span className="text-content-subtle text-xs">
({currencyFormatter(subValue / 100)})
</span>
)}
) : null}
</span>
</div>
<div className="xs:block hidden h-12">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function Header({
<Link href={`/${program.slug}`} className="animate-fade-in my-0.5 block">
{program.wordmark || program.logo ? (
<img
className="h-full max-h-7 w-full max-w-32"
className="max-h-7 max-w-32"
src={(program.wordmark ?? program.logo) as string}
/>
) : (
Expand Down
17 changes: 14 additions & 3 deletions apps/web/lib/actions/partners/create-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ export const createProgram = async ({

await getDomainOrThrow({ workspace, domain });

const programFolder = await prisma.folder.create({
data: {
const programFolder = await prisma.folder.upsert({
where: {
name_projectId: {
name: "Partner Links",
projectId: workspace.id,
},
},
update: {},
create: {
id: createId({ prefix: "fold_" }),
name: "Partner Links",
projectId: workspace.id,
Expand Down Expand Up @@ -142,15 +149,19 @@ export const createProgram = async ({
}),
},
}),

prisma.program.update({
where: {
id: program.id,
},
data: {
...(logoUrl && { logo: logoUrl }),
...(program.rewards && { defaultRewardId: program.rewards[0].id }),
...(program.rewards?.[0]?.id && {
defaultRewardId: program.rewards[0].id,
}),
},
}),

uploadedLogo &&
isStored(uploadedLogo) &&
storage.delete(uploadedLogo.replace(`${R2_URL}/`, "")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { createId } from "@/lib/api/create-id";
import { storage } from "@/lib/storage";
import { base64ImageSchema } from "@/lib/zod/schemas/misc";
import {
programResourceColorSchema,
programResourceFileSchema,
Expand All @@ -19,10 +20,17 @@ const baseResourceSchema = z.object({
name: z.string().min(1, "Name is required"),
});

// Schema for file-based resources (logos and files)
// Schema for logo resources
const logoResourceSchema = baseResourceSchema.extend({
resourceType: z.literal("logo"),
file: base64ImageSchema,
extension: z.string().nullish(),
});

// Schema for file resources
const fileResourceSchema = baseResourceSchema.extend({
resourceType: z.enum(["logo", "file"]),
file: z.string(), // Base64 encoded file
resourceType: z.literal("file"),
file: z.string(),
extension: z.string().nullish(),
});

Expand All @@ -34,6 +42,7 @@ const colorResourceSchema = baseResourceSchema.extend({

// Combined schema that can handle any resource type
const addResourceSchema = z.discriminatedUnion("resourceType", [
logoResourceSchema,
fileResourceSchema,
colorResourceSchema,
]);
Expand Down Expand Up @@ -69,6 +78,10 @@ export const addProgramResourceAction = authActionClient
if (resourceType === "logo" || resourceType === "file") {
const { file, extension } = parsedInput;

if (!file) {
throw new Error("File is required.");
}

// Upload the file to storage
const fileKey = `programs/${program.id}/${resourceType}s/${slugify(name || resourceType)}-${nanoid(4)}${extension ? `.${extension}` : ""}`;
const uploadResult = await storage.upload(
Expand Down Expand Up @@ -136,8 +149,4 @@ export const addProgramResourceAction = authActionClient
resources: updatedResources,
},
});

return {
success: true,
};
});
3 changes: 2 additions & 1 deletion apps/web/lib/actions/partners/update-partner-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import { PartnerProfileType } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { stripe } from "../../stripe";
import z from "../../zod";
import { base64ImageSchema } from "../../zod/schemas/misc";
import { authPartnerActionClient } from "../safe-action";

const updatePartnerProfileSchema = z
.object({
name: z.string(),
image: z.string().nullable(),
image: base64ImageSchema.nullish(),
description: z.string().nullable(),
country: z.enum(Object.keys(COUNTRIES) as [string, ...string[]]).nullable(),
profileType: z.nativeEnum(PartnerProfileType),
Expand Down
13 changes: 6 additions & 7 deletions apps/web/lib/zod/schemas/domains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { normalizeWorkspaceId } from "@/lib/api/workspace-id";
import z from "@/lib/zod";
import { booleanQuerySchema, getPaginationQuerySchema } from "./misc";
import {
base64ImageSchema,
booleanQuerySchema,
getPaginationQuerySchema,
} from "./misc";
import { parseUrlSchemaAllowEmpty } from "./utils";

export const RegisteredDomainSchema = z.object({
Expand Down Expand Up @@ -140,12 +144,7 @@ export const createDomainBodySchema = z.object({
"Provide context to your teammates in the link creation modal by showing them an example of a link to be shortened.",
)
.openapi({ example: "https://dub.co/help/article/what-is-dub" }),
logo: z
.string()
.trim()
.nullish()
.transform((v) => v || null)
.describe("The logo of the domain."),
logo: base64ImageSchema.nullish().describe("The logo of the domain."),
assetLinks: z
.string()
.nullish()
Expand Down
8 changes: 2 additions & 6 deletions apps/web/lib/zod/schemas/integration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { R2_URL } from "@dub/utils";
import { z } from "zod";
import { base64ImageSchema } from "./misc";

export const integrationSchema = z.object({
id: z.string(),
Expand Down Expand Up @@ -37,12 +38,7 @@ export const createIntegrationSchema = z.object({
message: "installUrl must be a valid URL",
})
.nullish(),
logo: z
.string()
.url({
message: "Please provide a valid URL for the logo",
})
.nullish(),
logo: base64ImageSchema.nullish().describe("The logo of the integration."),
description: z
.string()
.max(120, {
Expand Down
Loading