Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2252fee
wip PartnerStack import functionality
devkiran Jul 24, 2025
33f4b42
Update create-program.ts
devkiran Jul 24, 2025
7d26519
consistent naming
devkiran Jul 24, 2025
1b2e821
Refactor Rewardful import functionality to standardize naming convent…
devkiran Jul 24, 2025
33746e2
Refactor Tolt import functionality to use a unified payload schema an…
devkiran Jul 24, 2025
c60c907
Update schemas.ts
devkiran Jul 24, 2025
6ab3589
Update form.tsx
devkiran Jul 24, 2025
8ab433b
Update form.tsx
devkiran Jul 24, 2025
e637021
standardize naming conventions for customers and partners
devkiran Jul 24, 2025
84a1588
Update create-program.ts
devkiran Jul 24, 2025
4694a4d
fix build
devkiran Jul 24, 2025
bbb0161
Refactor Rewardful import functions to utilize a unified payload sche…
devkiran Jul 24, 2025
496bb59
Update set-rewardful-token.ts
devkiran Jul 24, 2025
2afd526
Update import-rewardful-modal.tsx
devkiran Jul 24, 2025
07a3c87
Enhance reward forms with onSuccess and isPending props for improved …
devkiran Jul 24, 2025
6ff7773
Refactor PartnerStack import functions to standardize handling of sta…
devkiran Jul 24, 2025
66499aa
Update update-stripe-customers.ts
devkiran Jul 24, 2025
1690f6e
Update import-commissions.ts
devkiran Jul 24, 2025
a71d5d7
Update import-customers.ts
devkiran Jul 24, 2025
9a5426e
Update import-links.ts
devkiran Jul 24, 2025
65cd7bb
Update import-partners.ts
devkiran Jul 24, 2025
4a85276
Merge branch 'main' into program-onboarding-updates
devkiran Jul 25, 2025
73ce342
Update import-commissions.ts
devkiran Jul 25, 2025
004f58c
Update form.tsx
devkiran Jul 25, 2025
3f619aa
Update import-partnerstack-form.tsx
devkiran Jul 25, 2025
20946fa
Merge branch 'main' into program-onboarding-updates
steven-tey Jul 25, 2025
a8586e2
Merge branch 'main' into program-onboarding-updates
steven-tey Jul 26, 2025
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
44 changes: 11 additions & 33 deletions apps/web/app/(ee)/api/cron/import/rewardful/route.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,33 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { importAffiliates } from "@/lib/rewardful/import-affiliates";
import { importCampaign } from "@/lib/rewardful/import-campaign";
import { importCommissions } from "@/lib/rewardful/import-commissions";
import { importReferrals } from "@/lib/rewardful/import-referrals";
import { importSteps } from "@/lib/rewardful/importer";
import { importCustomers } from "@/lib/rewardful/import-customers";
import { importPartners } from "@/lib/rewardful/import-partners";
import { rewardfulImportPayloadSchema } from "@/lib/rewardful/schemas";
import { NextResponse } from "next/server";
import { z } from "zod";

export const dynamic = "force-dynamic";

const schema = z.object({
programId: z.string(),
rewardId: z.string().optional(),
action: importSteps,
page: z.number().optional().default(1),
});

export async function POST(req: Request) {
try {
const rawBody = await req.text();
await verifyQstashSignature({ req, rawBody });

const { programId, rewardId, action, page } = schema.parse(
JSON.parse(rawBody),
);
const payload = rewardfulImportPayloadSchema.parse(JSON.parse(rawBody));

switch (action) {
switch (payload.action) {
case "import-campaign":
await importCampaign({
programId,
});
await importCampaign(payload);
break;
case "import-affiliates":
await importAffiliates({
programId,
rewardId,
page,
});
case "import-partners":
await importPartners(payload);
break;
case "import-referrals":
await importReferrals({
programId,
page,
});
case "import-customers":
await importCustomers(payload);
break;
case "import-commissions":
await importCommissions({
programId,
page,
});
await importCommissions(payload);
break;
}

Expand Down
25 changes: 9 additions & 16 deletions apps/web/app/(ee)/api/cron/import/tolt/route.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { cleanupPartners } from "@/lib/tolt/cleanup-partners";
import { importAffiliates } from "@/lib/tolt/import-affiliates";
import { importCommissions } from "@/lib/tolt/import-commissions";
import { importCustomers } from "@/lib/tolt/import-customers";
import { importLinks } from "@/lib/tolt/import-links";
import { importReferrals } from "@/lib/tolt/import-referrals";
import { importSteps } from "@/lib/tolt/importer";
import { importPartners } from "@/lib/tolt/import-partners";
import { toltImportPayloadSchema } from "@/lib/tolt/schemas";
import { updateStripeCustomers } from "@/lib/tolt/update-stripe-customers";
import { NextResponse } from "next/server";
import { z } from "zod";

export const dynamic = "force-dynamic";

const schema = z.object({
action: importSteps,
programId: z.string(),
startingAfter: z.string().optional(),
});

export async function POST(req: Request) {
try {
const rawBody = await req.text();
Expand All @@ -27,17 +20,17 @@ export async function POST(req: Request) {
rawBody,
});

const { action, ...payload } = schema.parse(JSON.parse(rawBody));
const payload = toltImportPayloadSchema.parse(JSON.parse(rawBody));

switch (action) {
case "import-affiliates":
await importAffiliates(payload);
switch (payload.action) {
case "import-partners":
await importPartners(payload);
break;
case "import-links":
await importLinks(payload);
break;
case "import-referrals":
await importReferrals(payload);
case "import-customers":
await importCustomers(payload);
break;
case "import-commissions":
await importCommissions(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import { cn } from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
useFormContext,
UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
import { toast } from "sonner";
import { ImportPartnerStackForm } from "./import-partnerstack-form";
import { ImportRewardfulForm } from "./import-rewardful-form";
import { ImportToltForm } from "./import-tolt-form";

Expand Down Expand Up @@ -64,25 +65,24 @@ type ImportSource = (typeof PROGRAM_IMPORT_SOURCES)[number];

export function Form() {
const router = useRouter();
const { id: workspaceId, slug: workspaceSlug, mutate } = useWorkspace();
const [hasSubmitted, setHasSubmitted] = useState(false);
const [selectedSource, setSelectedSource] = useState<ImportSource>(
PROGRAM_IMPORT_SOURCES[0],
);
const { id: workspaceId, slug: workspaceSlug, mutate } = useWorkspace();

const {
register,
handleSubmit,
watch,
setValue,
getValues,
formState: { isSubmitting },
} = useFormContext<ProgramData>();

const [programType, rewardful, tolt, amount] = watch([
const [programType, importSource, amount, type, defaultRewardType] = watch([
"programType",
"rewardful",
"tolt",
"importSource",
"amount",
"type",
"defaultRewardType",
]);

useEffect(() => {
Expand All @@ -96,17 +96,6 @@ export function Form() {
}
}, [programType]);

// Set the import source based on existing program data
useEffect(() => {
if (programType === "import") {
if (rewardful && rewardful.id) {
setSelectedSource(PROGRAM_IMPORT_SOURCES[0]);
} else if (tolt && tolt.id) {
setSelectedSource(PROGRAM_IMPORT_SOURCES[1]);
}
}
}, [programType, tolt, rewardful]);

const { executeAsync, isPending } = useAction(onboardProgramAction, {
onSuccess: () => {
router.push(`/${workspaceSlug}/program/new/partners`);
Expand All @@ -133,19 +122,47 @@ export function Form() {
});
};

const selectedSource = useMemo(() => {
return PROGRAM_IMPORT_SOURCES.find((source) => source.id === importSource);
}, [importSource]);

const renderImportForm = () => {
const isPendingAction = isSubmitting || isPending || hasSubmitted;

switch (selectedSource?.id) {
case "rewardful":
return (
<ImportRewardfulForm
register={register}
watch={watch}
setValue={setValue}
onSuccess={() => onSubmit(getValues())}
isPending={isPendingAction}
/>
);
case "tolt":
return (
<ImportToltForm
watch={watch}
setValue={setValue}
onSuccess={() => onSubmit(getValues())}
isPending={isPendingAction}
/>
);
case "partnerstack":
return (
<ImportPartnerStackForm
onSuccess={() => onSubmit(getValues())}
isPending={isPendingAction}
/>
);
default:
return null;
}
};

const buttonDisabled =
isSubmitting ||
isPending ||
hasSubmitted ||
(programType === "new" && !amount) ||
(programType === "import" &&
(!rewardful || !rewardful.id) &&
(!tolt || !tolt.id));

const hideContinueButton =
programType === "import" &&
(!rewardful || !rewardful.id) &&
(!tolt || !tolt.id);
programType === "new" && (!amount || !type || !defaultRewardType);

return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-10">
Expand Down Expand Up @@ -208,8 +225,14 @@ export function Form() {
items={
PROGRAM_IMPORT_SOURCES as unknown as InputSelectItemProps[]
}
selectedItem={selectedSource}
setSelectedItem={setSelectedSource}
selectedItem={selectedSource ?? null}
setSelectedItem={(item: ImportSource) => {
if (item) {
setValue("importSource", item.id, {
shouldDirty: true,
});
}
}}
className="w-full"
inputAttrs={{
placeholder: "Select import source",
Expand All @@ -229,24 +252,16 @@ export function Form() {
)}
</div>

{selectedSource.id === "rewardful" ? (
<ImportRewardfulForm
register={register}
watch={watch}
setValue={setValue}
/>
) : (
<ImportToltForm watch={watch} setValue={setValue} />
)}
{renderImportForm()}
</div>
)}

{!hideContinueButton && (
{programType === "new" && (
<Button
text="Continue"
className="w-full"
loading={isSubmitting || isPending || hasSubmitted}
disabled={buttonDisabled}
loading={isSubmitting || isPending}
disabled={buttonDisabled || hasSubmitted}
type="submit"
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"use client";

import { setPartnerStackTokenAction } from "@/lib/actions/partners/set-partnerstack-token";
import useWorkspace from "@/lib/swr/use-workspace";
import { Button, Input } from "@dub/ui";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";

export const ImportPartnerStackForm = ({
onSuccess,
isPending,
}: {
onSuccess: () => void;
isPending: boolean;
}) => {
const { id: workspaceId } = useWorkspace();
const [publicKey, setPublicKey] = useState("");
const [secretKey, setSecretKey] = useState("");

const { executeAsync, isPending: isSettingPartnerStackToken } = useAction(
setPartnerStackTokenAction,
{
onSuccess: () => {
onSuccess();
toast.success("PartnerStack credentials saved successfully!");
},
onError: ({ error }) => {
toast.error(error.serverError);
},
},
);

const onSubmit = async () => {
if (!workspaceId || !publicKey || !secretKey) {
toast.error("Please fill in all required fields.");
return;
}

await executeAsync({
workspaceId,
publicKey,
secretKey,
});
};

return (
<div className="space-y-6">
<div>
<label className="text-sm font-medium text-neutral-800">
PartnerStack Public Key
</label>
<Input
type="password"
placeholder="Public key"
className="mt-2 max-w-full"
value={publicKey}
onChange={(e) => setPublicKey(e.target.value)}
/>
<div className="mt-2 text-xs font-normal leading-[1.1] text-neutral-600">
Find your PartnerStack API keys in your{" "}
<Link
href="https://app.partnerstack.com/settings/integrations"
className="underline decoration-solid decoration-auto underline-offset-auto"
target="_blank"
rel="noopener noreferrer"
>
Settings
</Link>
</div>
</div>

<div>
<label className="text-sm font-medium text-neutral-800">
PartnerStack Secret Key
</label>
<Input
type="password"
placeholder="Secret key"
className="mt-2 max-w-full"
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
/>
</div>

<Button
text="Continue"
className="w-full"
disabled={!publicKey || !secretKey}
loading={isSettingPartnerStackToken || isPending}
onClick={onSubmit}
/>
</div>
);
};
Loading