+
Your social and web platforms
-
+
Verifying your social and web platforms will improve your reputation
score and rank you higher in our partner network.
-
);
diff --git a/apps/web/app/api/veriff/webhook/handle-decision-event.ts b/apps/web/app/api/veriff/webhook/handle-decision-event.ts
index e88ec86ece8..03fb5214b2f 100644
--- a/apps/web/app/api/veriff/webhook/handle-decision-event.ts
+++ b/apps/web/app/api/veriff/webhook/handle-decision-event.ts
@@ -1,5 +1,9 @@
-import { computeVeriffIdentityHash } from "@/lib/veriff/compute-veriff-identity-hash";
-import { VeriffDecisionEvent } from "@/lib/veriff/schema";
+import { detectDuplicateIdentityFraud } from "@/lib/api/fraud/detect-duplicate-identity-fraud";
+import {
+ VeriffDecisionEvent,
+ VeriffRiskLabel,
+ veriffRiskLabels,
+} from "@/lib/veriff/schema";
import {
mergeVeriffMetadata,
parseVeriffMetadata,
@@ -10,6 +14,7 @@ import PartnerIdentityVerified from "@dub/email/templates/partner-identity-verif
import { prisma } from "@dub/prisma";
import { IdentityVerificationStatus, Partner } from "@dub/prisma/client";
import { DUPLICATE_IDENTITY_DECLINE_REASON } from "@dub/utils";
+import { waitUntil } from "@vercel/functions";
import { logAndRespond } from "app/(ee)/api/cron/utils";
const veriffStatusMap: Record<
@@ -27,7 +32,8 @@ const veriffStatusMap: Record<
export const handleDecisionEvent = async ({
verification,
}: VeriffDecisionEvent) => {
- const { id, status, decisionTime, reason, attemptId } = verification;
+ const { id, status, decisionTime, reason, attemptId, riskLabels } =
+ verification;
let effectiveStatus = status;
@@ -55,7 +61,6 @@ export const handleDecisionEvent = async ({
// since we're skipping verified partners, by default identityVerifiedAt is null
let identityVerifiedAt: Date | null = null;
- let veriffIdentityHash: string | null | undefined = undefined;
let { sessionUrl, attemptCount, declineReason, sessionExpiresAt } =
parseVeriffMetadata(partner.veriffMetadata);
@@ -65,20 +70,28 @@ export const handleDecisionEvent = async ({
// If the verification was approved, check for country mismatch
if (effectiveStatus === "approved") {
- veriffIdentityHash = computeVeriffIdentityHash(verification);
- const isDuplicate = await checkDuplicateIdentity({
- partner,
- veriffIdentityHash,
- });
+ const hasDuplicateRiskLabel =
+ riskLabels &&
+ riskLabels?.length > 0 &&
+ riskLabels.some(({ label }) =>
+ veriffRiskLabels.includes(label as VeriffRiskLabel),
+ );
const isCountryMismatch = checkCountryMismatch({
partner,
verification,
});
- if (isDuplicate) {
+ if (hasDuplicateRiskLabel) {
effectiveStatus = "declined";
declineReason = DUPLICATE_IDENTITY_DECLINE_REASON;
+
+ waitUntil(
+ detectDuplicateIdentityFraud({
+ veriffSessionId: id,
+ riskLabels,
+ }),
+ );
} else if (isCountryMismatch) {
effectiveStatus = "declined";
declineReason = `Your document country (${verification.document?.country}) does not match your account country (${partner.country})`;
@@ -109,8 +122,6 @@ export const handleDecisionEvent = async ({
data: {
identityVerificationStatus: veriffStatusMap[effectiveStatus],
identityVerifiedAt,
- veriffIdentityHash:
- effectiveStatus === "approved" ? veriffIdentityHash : null,
veriffMetadata,
},
select: {
@@ -148,32 +159,6 @@ function checkCountryMismatch({
return partner.country.toUpperCase() !== veriffCountry;
}
-async function checkDuplicateIdentity({
- partner,
- veriffIdentityHash,
-}: {
- partner: Pick
;
- veriffIdentityHash: string | null;
-}): Promise {
- if (!veriffIdentityHash) {
- return false;
- }
-
- const duplicatePartner = await prisma.partner.findFirst({
- where: {
- veriffIdentityHash,
- id: {
- not: partner.id,
- },
- },
- select: {
- id: true,
- },
- });
-
- return !!duplicatePartner;
-}
-
async function sendEmailNotification({
partner,
attemptId,
diff --git a/apps/web/app/api/veriff/webhook/handle-session-event.ts b/apps/web/app/api/veriff/webhook/handle-session-event.ts
index d289de39a5d..b79b303c1a1 100644
--- a/apps/web/app/api/veriff/webhook/handle-session-event.ts
+++ b/apps/web/app/api/veriff/webhook/handle-session-event.ts
@@ -40,7 +40,6 @@ export const handleSessionEvent = async ({
},
data: {
identityVerificationStatus: action,
- veriffIdentityHash: null,
veriffMetadata,
},
});
diff --git a/apps/web/app/app.dub.co/(onboarding)/layout.tsx b/apps/web/app/app.dub.co/(onboarding)/layout.tsx
index acd1f82aad1..b1d46d6ddec 100644
--- a/apps/web/app/app.dub.co/(onboarding)/layout.tsx
+++ b/apps/web/app/app.dub.co/(onboarding)/layout.tsx
@@ -6,8 +6,10 @@ export default function Layout({ children }: PropsWithChildren) {
return (
<>
{children}
-
-
+
+
+
+
>
);
}
diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx
index c51d5cc1ac7..2104e3da54a 100644
--- a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx
+++ b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/domain/default-domain-selector.tsx
@@ -16,7 +16,7 @@ export function DefaultDomainSelector() {
return (
<>
-
+
-
-
-
+
+
-
{children}
+
+
+
+
+
- {/* Empty div to center main content */}
-
+ {/* Empty div to center main content on desktop */}
+
>
);
diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/plan/plan-selector.tsx b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/plan/plan-selector.tsx
index a4c75a9bab0..0f6b3c6b22b 100644
--- a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/plan/plan-selector.tsx
+++ b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/plan/plan-selector.tsx
@@ -33,40 +33,48 @@ export function PlanSelector({ product }: { product: OnboardingProduct }) {
: [PRO_PLAN, BUSINESS_PLAN, ADVANCED_PLAN];
const [period, setPeriod] = useState<"monthly" | "yearly">("monthly");
- const [mobilePlanIndex, setMobilePlanIndex] = useState(0);
+ const [mobilePlanIndex, setMobilePlanIndex] = useState(() => {
+ const defaultPlanName = product === "partners" ? "Advanced" : "Business";
+ return Math.max(
+ 0,
+ plans.findIndex((plan) => plan.name === defaultPlanName),
+ );
+ });
return (
-
-
+
+
- {plans.map((plan) => {
- const features = PRICING_PLAN_MAIN_FEATURES[product][plan.name] || [];
+ )}
+ style={
+ {
+ "--cols": plans.length,
+ "--index": mobilePlanIndex,
+ } as CSSProperties
+ }
+ >
+ {plans.map((plan) => {
+ const features = PRICING_PLAN_MAIN_FEATURES[product][plan.name] || [];
return (
+ key={plan.name}
+ className={cn(
+ "flex flex-col border-y border-l border-neutral-200 bg-white first:rounded-l-lg last:rounded-r-lg last:border-r",
+ "max-lg:overflow-hidden max-lg:rounded-lg max-lg:border-0 max-lg:ring-1 max-lg:ring-inset max-lg:ring-neutral-200",
+ product === "links" &&
+ plan.name === "Business" &&
+ "bg-gradient-to-b from-orange-50 to-40%",
+ product === "partners" &&
+ plan.name === "Advanced" &&
+ "bg-gradient-to-b from-violet-50 to-40%",
+ )}
+ >
@@ -288,7 +296,8 @@ export function PlanSelector({ product }: { product: OnboardingProduct }) {
)}
);
- })}
+ })}
+
);
diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/products/product-selector.tsx b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/products/product-selector.tsx
index 3262536a30d..fd0c5c6b700 100644
--- a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/products/product-selector.tsx
+++ b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/products/product-selector.tsx
@@ -7,14 +7,6 @@ import { ReactNode } from "react";
import { useOnboardingProgress } from "../../use-onboarding-progress";
const products = {
- links: {
- image: "https://assets.dub.co/icons/link.webp",
- title: "Dub Links",
- href: "https://dub.co/links",
- description:
- "[Short links](https://dub.co/help/category/link-management), [QR codes](https://dub.co/help/article/custom-qr-codes), [real-time analytics](https://dub.co/help/article/dub-analytics), and [conversion tracking](https://dub.co/docs/conversions/quickstart).",
- paidPlanRequired: false,
- },
partners: {
image: "https://assets.dub.co/icons/trophy.webp",
title: "Dub Partners",
@@ -23,11 +15,19 @@ const products = {
"Modern [affiliate programs](https://dub.co/partners) with [global payouts](https://dub.co/help/article/partner-payouts) and [accurate attribution](https://dub.co/help/article/program-analytics).",
paidPlanRequired: true,
},
+ links: {
+ image: "https://assets.dub.co/icons/link.webp",
+ title: "Dub Links",
+ href: "https://dub.co/links",
+ description:
+ "[Short links](https://dub.co/help/category/link-management), [QR codes](https://dub.co/help/article/custom-qr-codes), [real-time analytics](https://dub.co/help/article/dub-analytics), and [conversion tracking](https://dub.co/docs/conversions/quickstart).",
+ paidPlanRequired: false,
+ },
};
export function ProductSelector() {
return (
-
+
{Object.entries(products).map(([key, product]) => (
-
+
{DEFAULT_REWARD_TYPES.map(
({ key, label, description, mostCommon }) => {
const isSelected = key === defaultRewardType;
@@ -199,7 +199,7 @@ export function Form() {
Commission structure
-
+
{COMMISSION_TYPES.map(
({ value, label, shortDescription }) => {
const isSelected = value === commissionStructure;
@@ -307,7 +307,7 @@ export function Form() {
Payout model
-
+
{PAYOUT_MODELS.map(
({ key, label, description, mostCommon }) => {
const isSelected = key === type;
diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/step-page.tsx b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/step-page.tsx
index bf930c78972..5d6d50af38a 100644
--- a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/step-page.tsx
+++ b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/step-page.tsx
@@ -25,7 +25,7 @@ export function StepPage({
return (
-
- You're signed in as{" "}
- {session ? (
-
{session.user?.email}
- ) : (
-
- )}
+
+
+
+ You're signed in as{" "}
+ {session ? (
+ {session.user?.email}
+ ) : (
+
+ )}
+
+
+
+
-
);
}
diff --git a/apps/web/lib/api/fraud/constants.ts b/apps/web/lib/api/fraud/constants.ts
index dfe5f651f9e..891f859d88b 100644
--- a/apps/web/lib/api/fraud/constants.ts
+++ b/apps/web/lib/api/fraud/constants.ts
@@ -56,6 +56,16 @@ export const FRAUD_RULES: FraudRuleInfo[] = [
severity: "high",
configurable: true,
},
+ // Not visible in the UI
+ {
+ type: "partnerDuplicateAccount",
+ name: "Duplicate account detected",
+ description:
+ "This partner was flagged by our system for having 2 or more Dub accounts. Please review to prevent abuse of program restrictions, caps, or bonuses.",
+ scope: "partner",
+ severity: "low",
+ configurable: false,
+ },
{
type: "partnerEmailDomainMismatch",
name: "Email domain mismatch with website",
diff --git a/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts b/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts
new file mode 100644
index 00000000000..977ecc5aac4
--- /dev/null
+++ b/apps/web/lib/api/fraud/detect-duplicate-identity-fraud.ts
@@ -0,0 +1,128 @@
+import { CreateFraudEventInput } from "@/lib/types";
+import {
+ VeriffDecisionEvent,
+ VeriffRiskLabel,
+ veriffRiskLabels,
+} from "@/lib/veriff/schema";
+import { INACTIVE_ENROLLMENT_STATUSES } from "@/lib/zod/schemas/partners";
+import { prisma } from "@dub/prisma";
+import { FraudRuleType, ProgramEnrollment } from "@dub/prisma/client";
+import { createFraudEvents } from "./create-fraud-events";
+import { isFraudRuleEnabled } from "./get-merged-fraud-rules";
+
+// Check for duplicate identities: if multiple partners share the same identity,
+// create fraud events for all their active program enrollments to flag potential fraud
+export async function detectDuplicateIdentityFraud({
+ veriffSessionId,
+ riskLabels,
+}: {
+ veriffSessionId: string;
+ riskLabels: VeriffDecisionEvent["verification"]["riskLabels"];
+}) {
+ if (!riskLabels || riskLabels.length === 0) {
+ console.log("[detectDuplicateIdentityFraud] No risk labels provided.");
+
+ return;
+ }
+
+ let veriffSessionIds = riskLabels
+ .filter(({ label }) => veriffRiskLabels.includes(label as VeriffRiskLabel))
+ .map(({ sessionIds }) => sessionIds)
+ .flat();
+
+ // Add the current veriff session id to the list
+ veriffSessionIds.push(veriffSessionId);
+
+ // Remove duplicates
+ veriffSessionIds = [...new Set(veriffSessionIds)];
+
+ if (veriffSessionIds.length === 0) {
+ console.log(
+ "[detectDuplicateIdentityFraud] No veriff session ids provided.",
+ );
+ return;
+ }
+
+ let programEnrollments = await prisma.programEnrollment.findMany({
+ where: {
+ partner: {
+ veriffSessionId: {
+ in: veriffSessionIds,
+ },
+ },
+ },
+ select: {
+ programId: true,
+ partnerId: true,
+ status: true,
+ program: {
+ select: {
+ fraudRules: true,
+ },
+ },
+ },
+ });
+
+ if (programEnrollments.length === 0) {
+ console.log("[detectDuplicateIdentityFraud] No program enrollments found.");
+ return;
+ }
+
+ // Filter out program enrollments where the partnerDuplicateAccount rule is disabled
+ programEnrollments = programEnrollments.filter((enrollment) =>
+ isFraudRuleEnabled({
+ fraudRules: enrollment.program.fraudRules,
+ ruleType: FraudRuleType.partnerDuplicatePayoutMethod, // TODO: Change to partnerDuplicateAccount
+ }),
+ );
+
+ // Group partners by program enrollment
+ let partnersByProgram = programEnrollments.reduce((map, e) => {
+ if (!map.has(e.programId)) {
+ map.set(e.programId, []);
+ }
+
+ map.get(e.programId)!.push({
+ partnerId: e.partnerId,
+ status: e.status,
+ });
+
+ return map;
+ }, new Map
[]>());
+
+ // Filter out programs with only one partner
+ partnersByProgram = new Map(
+ Array.from(partnersByProgram.entries()).filter(
+ ([_, partners]) => partners.length > 1,
+ ),
+ );
+
+ if (partnersByProgram.size === 0) {
+ console.log("[detectDuplicateIdentityFraud] No multiple partners found.");
+ return;
+ }
+
+ const fraudEvents: CreateFraudEventInput[] = [];
+
+ for (const [programId, partners] of partnersByProgram.entries()) {
+ for (const sourcePartner of partners) {
+ if (INACTIVE_ENROLLMENT_STATUSES.includes(sourcePartner.status)) {
+ continue;
+ }
+
+ for (const enrolledPartner of partners) {
+ fraudEvents.push({
+ programId,
+ partnerId: sourcePartner.partnerId,
+ type: FraudRuleType.partnerDuplicateAccount,
+ metadata: {
+ duplicatePartnerId: enrolledPartner.partnerId,
+ riskLabels,
+ },
+ });
+ }
+ }
+ }
+
+ await createFraudEvents(fraudEvents);
+}
diff --git a/apps/web/lib/api/fraud/execute-fraud-rule.ts b/apps/web/lib/api/fraud/execute-fraud-rule.ts
index 421adda3a84..d8ef692a4da 100644
--- a/apps/web/lib/api/fraud/execute-fraud-rule.ts
+++ b/apps/web/lib/api/fraud/execute-fraud-rule.ts
@@ -26,6 +26,7 @@ const FRAUD_RULES_REGISTRY: Record<
partnerDuplicatePayoutMethod: defineFraudRuleStub(
"partnerDuplicatePayoutMethod",
),
+ partnerDuplicateAccount: defineFraudRuleStub("partnerDuplicateAccount"),
};
// Execute a fraud rule with the given context and configuration
diff --git a/apps/web/lib/api/fraud/utils.ts b/apps/web/lib/api/fraud/utils.ts
index 05f00bf7158..f9233b5dcbd 100644
--- a/apps/web/lib/api/fraud/utils.ts
+++ b/apps/web/lib/api/fraud/utils.ts
@@ -90,6 +90,7 @@ function getIdentityFieldsForFraudEvent({
};
case "partnerDuplicatePayoutMethod":
+ case "partnerDuplicateAccount":
return {
duplicatePartnerId: eventMetadata?.duplicatePartnerId,
};
@@ -130,13 +131,15 @@ export function createGroupCompositeKey(
}
// Determine the correct partnerId for a fraud event.
-// For duplicate payout method events, uses the duplicatePartnerId from metadata.
export function getPartnerIdForFraudEvent(
event: Pick,
) {
const metadata = event.metadata as Record | undefined;
- if (event.type === "partnerDuplicatePayoutMethod") {
+ if (
+ event.type === "partnerDuplicatePayoutMethod" ||
+ event.type === "partnerDuplicateAccount"
+ ) {
return metadata?.duplicatePartnerId ?? event.partnerId;
}
diff --git a/apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts b/apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
index c3d91522660..695e228d2ba 100644
--- a/apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
+++ b/apps/web/lib/api/partner-profile/get-partner-earnings-timeseries.ts
@@ -56,7 +56,7 @@ export async function getPartnerEarningsTimeseries({
SUM(earnings) AS earnings
FROM Commission
WHERE
- earnings > 0
+ earnings != 0
AND programId = ${program.id}
AND partnerId = ${partnerId}
AND createdAt >= ${startDate}
diff --git a/apps/web/lib/api/partners/sync-total-commissions.ts b/apps/web/lib/api/partners/sync-total-commissions.ts
index efa08f63cce..c842052a3b4 100644
--- a/apps/web/lib/api/partners/sync-total-commissions.ts
+++ b/apps/web/lib/api/partners/sync-total-commissions.ts
@@ -37,15 +37,10 @@ async function aggregateAndUpdateTotalCommissions({
export const syncTotalCommissions = async ({
partnerId,
programId,
- mode = "queue",
}: {
partnerId: string;
programId: string;
- mode?: "queue" | "direct";
}) => {
- if (mode === "direct") {
- return await aggregateAndUpdateTotalCommissions({ partnerId, programId });
- }
try {
return await publishPartnerActivityEvent({
programId,
diff --git a/apps/web/lib/auth/partner.ts b/apps/web/lib/auth/partner.ts
index 84031b2854a..d9a5053f3ab 100644
--- a/apps/web/lib/auth/partner.ts
+++ b/apps/web/lib/auth/partner.ts
@@ -224,9 +224,6 @@ export const withPartnerProfile = (
salesChannels: true,
platforms: true,
},
- omit: {
- veriffIdentityHash: true,
- },
},
},
});
diff --git a/apps/web/lib/veriff/compute-veriff-identity-hash.ts b/apps/web/lib/veriff/compute-veriff-identity-hash.ts
deleted file mode 100644
index 98895166ae0..00000000000
--- a/apps/web/lib/veriff/compute-veriff-identity-hash.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { createHash } from "crypto";
-import { VeriffDecisionEvent } from "./schema";
-
-export function computeVeriffIdentityHash(
- verification: VeriffDecisionEvent["verification"],
-) {
- const { person, document } = verification;
- const documentNumber = document?.number?.trim();
- const documentCountry = document?.country?.trim();
- const firstName = person?.firstName?.trim();
- const lastName = person?.lastName?.trim();
- const dateOfBirth = person?.dateOfBirth?.trim();
-
- // Prefer document number (passport/ID number) — strongest unique signal
- if (documentNumber) {
- const input = [
- "doc",
- documentNumber.toLowerCase(),
- ...(documentCountry ? [documentCountry.toUpperCase()] : []),
- ].join("|");
- return createHash("sha256").update(input).digest("hex");
- }
-
- // Fall back to name + date of birth
- if ((firstName || lastName) && dateOfBirth) {
- const input = [
- "person",
- ...(firstName ? [firstName.toLowerCase()] : []),
- ...(lastName ? [lastName.toLowerCase()] : []),
- dateOfBirth,
- ].join("|");
- return createHash("sha256").update(input).digest("hex");
- }
-
- return null;
-}
diff --git a/apps/web/lib/veriff/schema.ts b/apps/web/lib/veriff/schema.ts
index 03e1e4e50a4..a6221b8e29a 100644
--- a/apps/web/lib/veriff/schema.ts
+++ b/apps/web/lib/veriff/schema.ts
@@ -64,6 +64,16 @@ export const veriffDecisionEventSchema = z.object({
country: z.string().nullable(),
})
.optional(),
+ riskLabels: z
+ .array(
+ z.object({
+ label: z.string(),
+ category: z.string(),
+ sessionIds: z.array(z.string()),
+ }),
+ )
+ .nullish()
+ .describe("Only sent on production environment."),
}),
});
@@ -73,3 +83,12 @@ export const veriffEventSchema = z.union([
veriffSessionEventSchema,
veriffDecisionEventSchema,
]);
+
+// https://help.veriff.com/en/articles/3410712-risk-insights-and-crosslinks
+// This schema only includes the categories we care about; others may exist but are ignored.
+export const veriffRiskLabels = [
+ "person_previously_approved",
+ "document_previously_approved",
+] as const;
+
+export type VeriffRiskLabel = (typeof veriffRiskLabels)[number];
diff --git a/apps/web/lib/zod/schemas/fraud.ts b/apps/web/lib/zod/schemas/fraud.ts
index ce5343d2a56..100bb7f0263 100644
--- a/apps/web/lib/zod/schemas/fraud.ts
+++ b/apps/web/lib/zod/schemas/fraud.ts
@@ -252,6 +252,7 @@ export const updateFraudRuleSettingsSchema = z.object({
customerEmailSuspiciousDomain: toggleOnlyFraudRuleSchema,
partnerCrossProgramBan: toggleOnlyFraudRuleSchema,
partnerDuplicatePayoutMethod: toggleOnlyFraudRuleSchema,
+ partnerDuplicateAccount: toggleOnlyFraudRuleSchema,
});
const baseFraudEventSchema = z.object({
@@ -326,6 +327,8 @@ export const fraudEventSchemas = {
}),
partnerDuplicatePayoutMethod: baseFraudEventSchema,
+
+ partnerDuplicateAccount: baseFraudEventSchema,
};
export const fraudAlertSchema = z.object({
diff --git a/apps/web/playwright/partners/onboarding.spec.ts b/apps/web/playwright/partners/onboarding.spec.ts
index 3e46d57c2fe..ef8dc689224 100644
--- a/apps/web/playwright/partners/onboarding.spec.ts
+++ b/apps/web/playwright/partners/onboarding.spec.ts
@@ -19,9 +19,7 @@ test.describe("Partner onboarding", () => {
await expect(page.locator('input[name="name"]').first()).toBeVisible();
await expect(page.getByText("Profile image")).toBeVisible();
await expect(page.getByLabel("Country")).toBeVisible();
- await expect(
- page.getByLabel(/Description/, { exact: false }),
- ).toBeVisible();
+ await expect(page.getByText("About you")).toBeVisible();
await expect(page.getByText("Profile Type")).toBeVisible();
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
});
diff --git a/apps/web/scripts/programs/update-commissions-canceled.ts b/apps/web/scripts/programs/update-commissions-canceled.ts
index d6b0b8ae589..525f7a7f5b9 100644
--- a/apps/web/scripts/programs/update-commissions-canceled.ts
+++ b/apps/web/scripts/programs/update-commissions-canceled.ts
@@ -91,7 +91,6 @@ async function main() {
await syncTotalCommissions({
partnerId,
programId,
- mode: "direct",
});
}
}
diff --git a/apps/web/tests/fraud/fraud-groups.test.ts b/apps/web/tests/fraud/fraud-groups.test.ts
index 3c36b2dfb88..b54f0534075 100644
--- a/apps/web/tests/fraud/fraud-groups.test.ts
+++ b/apps/web/tests/fraud/fraud-groups.test.ts
@@ -66,6 +66,7 @@ describe.concurrent("/fraud/groups", async () => {
FraudRuleType.customerEmailSuspiciousDomain,
FraudRuleType.referralSourceBanned,
FraudRuleType.paidTrafficDetected,
+ FraudRuleType.partnerDuplicateAccount,
];
for (const fraudType of typesToTest) {
diff --git a/apps/web/ui/analytics/toggle.tsx b/apps/web/ui/analytics/toggle.tsx
index 442dd4302aa..521ae6763a2 100644
--- a/apps/web/ui/analytics/toggle.tsx
+++ b/apps/web/ui/analytics/toggle.tsx
@@ -93,7 +93,7 @@ export function AnalyticsToggle({
const dateRangePicker = (
diff --git a/apps/web/ui/layout/toolbar/toolbar.tsx b/apps/web/ui/layout/toolbar/toolbar.tsx
index 162e8f14512..84ada930ad4 100644
--- a/apps/web/ui/layout/toolbar/toolbar.tsx
+++ b/apps/web/ui/layout/toolbar/toolbar.tsx
@@ -17,8 +17,8 @@ export default function Toolbar(props: ToolbarProps) {
)}
{props.show?.includes("help") && (
-
diff --git a/apps/web/ui/partners/fraud-risks/fraud-events-tables/index.tsx b/apps/web/ui/partners/fraud-risks/fraud-events-tables/index.tsx
index 30164aa620f..3ea29fd446f 100644
--- a/apps/web/ui/partners/fraud-risks/fraud-events-tables/index.tsx
+++ b/apps/web/ui/partners/fraud-risks/fraud-events-tables/index.tsx
@@ -15,6 +15,7 @@ const FRAUD_EVENTS_TABLES: Partial
> =
paidTrafficDetected: FraudPaidTrafficDetectedTable,
partnerCrossProgramBan: FraudCrossProgramBanTable,
partnerDuplicatePayoutMethod: FraudPartnerInfoTable,
+ partnerDuplicateAccount: FraudPartnerInfoTable,
};
export function FraudEventsTableWrapper({
diff --git a/apps/web/ui/partners/partner-platforms-form.tsx b/apps/web/ui/partners/partner-platforms-form.tsx
index 0621f531838..ef7c42f288b 100644
--- a/apps/web/ui/partners/partner-platforms-form.tsx
+++ b/apps/web/ui/partners/partner-platforms-form.tsx
@@ -326,24 +326,26 @@ export const PartnerPlatformsForm = forwardRef<
youtube.com
-
- @
-
- onPasteSocial(e, "youtube")}
- {...register("youtube")}
- />
+
+
+ @
+
+ onPasteSocial(e, "youtube")}
+ {...register("youtube")}
+ />
+
}
variant={variant}
@@ -461,24 +463,26 @@ export const PartnerPlatformsForm = forwardRef<
tiktok.com
-
- @
-
-
onPasteSocial(e, "tiktok")}
- {...register("tiktok")}
- />
+
+
+ @
+
+ onPasteSocial(e, "tiktok")}
+ {...register("tiktok")}
+ />
+
}
variant={variant}
diff --git a/apps/web/ui/workspaces/create-workspace-form.tsx b/apps/web/ui/workspaces/create-workspace-form.tsx
index 9ae86d7d722..009cb80ffec 100644
--- a/apps/web/ui/workspaces/create-workspace-form.tsx
+++ b/apps/web/ui/workspaces/create-workspace-form.tsx
@@ -139,7 +139,7 @@ export function CreateWorkspaceForm({
-
+
app.{process.env.NEXT_PUBLIC_APP_DOMAIN}