Skip to content
Open
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
173 changes: 32 additions & 141 deletions apps/web/app/api/user/brands/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,147 +360,38 @@ export const POST = withSession(async ({ req, session }) => {
{ status: 500 },
);
}
} else if (advertiserId === "5") {
interface Campaign {
CampaignId: string;
AdvertiserName: string;
CampaignUrl: string;
}

interface ApiResponse {
"@total": string;
"@numpages": string;
"@page": string;
"@nextpageuri": string;
Campaigns: Campaign[];
}

try {
if (!accountId || !encryptedApiKey) {
return NextResponse.json(
{ error: "Missing Impact.com credentials." },
{ status: 400 },
);
}

const apiKey = decrypt(encryptedApiKey);
const baseUrl = `https://api.impact.com/Mediapartners/${accountId}/Campaigns`;
const headers = {
Accept: "application/json",
Authorization: `Basic ${Buffer.from(`${accountId}:${apiKey}`).toString("base64")}`,
};
const params = new URLSearchParams({
InsertionOrderStatus: "Active",
PageSize: "100", // Maximum allowed page size
});

let allCampaigns: Campaign[] = [];
let nextPageUri = `${baseUrl}?${params}`;

while (nextPageUri) {
const response = await fetch(nextPageUri, { headers });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = (await response.json()) as ApiResponse;
allCampaigns = allCampaigns.concat(data.Campaigns);

nextPageUri = data["@nextpageuri"]
? `https://api.impact.com${data["@nextpageuri"]}`
: "";
}

// Process campaigns and update database
const processedBrands = await Promise.all(
allCampaigns.map(async (campaign) => {
const brandName = campaign.AdvertiserName;
const brandId = campaign.CampaignId;
const advertiserUrl = campaign.CampaignUrl;
const modifiedUrl = extractBaseUrlUpdated(advertiserUrl);

if (modifiedUrl) {
const brand = await prisma.brand.findFirst({
where: {
url: modifiedUrl,
advertisers: {
some: {
brandIdAtAdvertiser: brandId,
advertiserId,
},
},
},
include: {
advertisers: {
where: { advertiserId: advertiserId },
},
userBrandRelationships: {
where: { userId: session.user.id, advertiserId },
},
},
});

if (!brand) {
const newBrand = await prisma.brand.create({
data: {
name: brandName,
url: modifiedUrl,
advertisers: {
create: {
brandIdAtAdvertiser: brandId,
advertiserId,
},
},
},
include: {
advertisers: {
where: { advertiserId: advertiserId },
},
},
});

const userBrandRelationship =
await prisma.userBrandRelationship.create({
data: {
userId: session.user.id,
brandId: newBrand.id,
advertiserId,
userAdvertiserRelationId: relationship.id,
brandAdvertiserRelationId: newBrand.advertisers[0].id,
},
});

return { brand: newBrand, userBrandRelationship };
}

if (brand) {
let userBrandRelationship = brand.userBrandRelationships[0];
if (!userBrandRelationship) {
userBrandRelationship =
await prisma.userBrandRelationship.create({
data: {
userId: session.user.id,
brandId: brand.id,
advertiserId,
userAdvertiserRelationId: relationship.id,
brandAdvertiserRelationId: brand.advertisers[0].id,
},
});
}

return { brand, userBrandRelationship };
}
}
return null;
}),
);

const validBrands = processedBrands.filter(Boolean);
return NextResponse.json(validBrands);
} catch (error) {
console.error("Error processing Impact.com advertiser:", error);
return NextResponse.json(
{ error: "Internal server error" },
// } else if (advertiserId === "5") {
// // Partnerize-specific processing
// try {
// const apiKey = decrypt(encryptedApiKey);
// const partnerizeUrl = "https://api.partnerize.com/v1/affiliate/link";

// const response = await fetch(partnerizeUrl, {
// method: "POST",
// headers: {
// Authorization: `Bearer ${apiKey}`,
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// url: processedUrl,
// campaign_id: accountId,
// }),
// });

// if (response.ok) {
// const data = await response.json();
// return NextResponse.json({ affiliateUrl: data.affiliate_link_url });
// } else {
// const errorData = await response.json();
// return NextResponse.json(
// { error: `Partnerize API error: ${errorData.message}` },
// { status: response.status },
// );
// }
// } catch (error) {
// console.error("Error processing Partnerize advertiser:", error);
// return NextResponse.json(
// { error: "Internal server error" },
{ status: 500 },
);
}
Expand Down
58 changes: 21 additions & 37 deletions apps/web/lib/api/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,65 +692,49 @@ export async function processLink({
console.error("Error generating affiliate link:", error);
}
} else if (advertiserId === "5") {
// Impact.com

interface ImpactApiResponse {
TrackingURL: string;
// Add other fields that might be in the response
Status?: string;
Message?: string;
}

const userAdvertiserRelation =
userBrandRelationship.userAdvertiserRelation;
const impactAccountId = userAdvertiserRelation.accountId || "";
const brandId =
userBrandRelationship.brandAdvertiserRelation.brandIdAtAdvertiser;
const impactApiKey = decrypt(
userAdvertiserRelation.encryptedApiKey || "",
);

if (!impactApiKey || !impactAccountId) {
const apiKey = decrypt(userAdvertiserRelation.encryptedApiKey || "");
const accountId = userAdvertiserRelation.accountId || "";

if (!apiKey || !accountId) {
return {
link: payload,
error: "Missing credentials for Impact.com affiliate program.",
error: "Missing credentials for Partnerize affiliate program.",
code: "unprocessable_entity",
};
}

const impactUrl = `https://api.impact.com/Mediapartners/${impactAccountId}/Programs/${brandId}/TrackingLinks`;
const encodedUrl = encodeURIComponent(processedUrl);

// Add the deep link as a query parameter
const urlWithParams = `${impactUrl}?DeepLink=${encodedUrl}`;
const partnerizeUrl = "https://api.partnerize.com/v1/affiliate/link";

const headers = {
Accept: "application/json",
// Using Basic Auth instead of Bearer token
Authorization: `Basic ${Buffer.from(`${impactAccountId}:${impactApiKey}`).toString("base64")}`,
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};

const data = {
url: processedUrl,
campaign_id: accountId,
};

try {
const response = await fetch(urlWithParams, {
method: "POST", // Changed from POST to GET
const response = await fetch(partnerizeUrl, {
method: "POST",
headers,
// Removed body since we're using GET
body: JSON.stringify(data),
});

if (response.ok) {
const responseData = (await response.json()) as ImpactApiResponse;

if (!responseData.TrackingURL) {
throw new Error("No tracking URL found in response");
}

clickUrl = responseData.TrackingURL;
const responseJson = await response.json();
const affiliateUrl = responseJson.affiliate_link_url || "";
clickUrl = affiliateUrl;
} else {
const errorData = await response.json();
console.error(`Error: ${errorData}`);
console.error(`Partnerize API error: ${errorData.message}`);
}
} catch (error) {
console.error("Error generating Impact.com affiliate link:", error);
console.error("Error generating Partnerize affiliate link:", error);
}
}
}
Expand Down
59 changes: 59 additions & 0 deletions apps/web/ui/modals/add-edit-network-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,65 @@ function AddEditNetworkModal({
</div>
</div>
</>
) : advertiserId === "5" ? ( // Partnerize
<>
<div>
<div className="flex items-center justify-between">
<label htmlFor={`partnerizeApiKey-${randomIdx}`} className="block text-sm font-medium text-gray-700">
API Key
</label>
<a
href="https://docs.partnerize.com"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-gray-500"
>
<HelpCircle className="h-4 w-4" />
</a>
</div>
<div className="relative mt-1 flex rounded-md shadow-sm">
<input
name="apiKey"
id={`partnerizeApiKey-${randomIdx}`}
placeholder="Enter your Partnerize API Key"
value={partialApiKey ?? ""}
required
autoComplete="off"
onChange={(e) => handleInputChange("partialApiKey", e.target.value)}
className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 block w-full rounded-md focus:outline-none sm:text-sm"
aria-invalid="true"
/>
</div>
</div>
<div>
<div className="flex items-center justify-between">
<label htmlFor={`partnerizeAccountId-${randomIdx}`} className="block text-sm font-medium text-gray-700">
Account ID
</label>
<a
href="https://docs.partnerize.com"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-gray-500"
>
<HelpCircle className="h-4 w-4" />
</a>
</div>
<div className="relative mt-1 flex rounded-md shadow-sm">
<input
name="accountId"
id={`partnerizeAccountId-${randomIdx}`}
placeholder="Enter your Partnerize Account ID"
value={accountId ?? ""}
required
autoComplete="off"
onChange={(e) => handleInputChange("accountId", e.target.value)}
className="border-gray-300 text-gray-900 placeholder-gray-300 focus:border-gray-500 focus:ring-gray-500 block w-full rounded-md focus:outline-none sm:text-sm"
aria-invalid="true"
/>
</div>
</div>
</>
) : null}
</div>

Expand Down