Created
September 24, 2025 10:32
-
-
Save charanjit-singh/b183df63c9830dd0f943f7399b65631c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cookies from 'js-cookie'; | |
// Referral cookie name | |
const REFERRAL_COOKIE_NAME = 'indiekit_referrals'; | |
// Cookie expiration days (45 days) | |
const COOKIE_EXPIRY_DAYS = 45; | |
// Max referrals to store | |
const MAX_REFERRALS = 5; | |
interface ReferralEntry { | |
code: string; | |
timestamp: number; | |
} | |
/** | |
* Get all stored referrals from the cookie | |
*/ | |
export function getStoredReferrals(): ReferralEntry[] { | |
try { | |
const cookieValue = Cookies.get(REFERRAL_COOKIE_NAME); | |
if (!cookieValue) return []; | |
return JSON.parse(cookieValue); | |
} catch (error) { | |
console.error('Error parsing referral cookie:', error); | |
// If the cookie is corrupt, reset it | |
Cookies.remove(REFERRAL_COOKIE_NAME); | |
return []; | |
} | |
} | |
/** | |
* Add a new referral code to the cookie | |
*/ | |
export function addReferral(refCode: string): void { | |
if (!refCode) return; | |
try { | |
const referrals = getStoredReferrals(); | |
// Check if this referral code already exists | |
const existingReferralIndex = referrals.findIndex(ref => ref.code === refCode); | |
if (existingReferralIndex >= 0) { | |
// Code already exists, do nothing to preserve the original timestamp | |
return; | |
} | |
// Add new referral with current timestamp | |
const newReferral: ReferralEntry = { | |
code: refCode, | |
timestamp: Date.now() | |
}; | |
// Add to the beginning, limit to max number of referrals | |
const updatedReferrals = [newReferral, ...referrals].slice(0, MAX_REFERRALS); | |
// Save back to cookie | |
Cookies.set(REFERRAL_COOKIE_NAME, JSON.stringify(updatedReferrals), { | |
expires: COOKIE_EXPIRY_DAYS, | |
sameSite: 'lax', | |
path: '/' | |
}); | |
} catch (error) { | |
console.error('Error updating referral cookie:', error); | |
} | |
} | |
/** | |
* Clean up expired referrals (older than 45 days) | |
*/ | |
export function cleanupExpiredReferrals(): void { | |
try { | |
const referrals = getStoredReferrals(); | |
const now = Date.now(); | |
const expiryTime = COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000; // 45 days in milliseconds | |
// Filter out expired referrals | |
const validReferrals = referrals.filter(ref => { | |
return (now - ref.timestamp) < expiryTime; | |
}); | |
// Only update if we removed some expired referrals | |
if (validReferrals.length < referrals.length) { | |
Cookies.set(REFERRAL_COOKIE_NAME, JSON.stringify(validReferrals), { | |
expires: COOKIE_EXPIRY_DAYS, | |
sameSite: 'lax', | |
path: '/' | |
}); | |
} | |
} catch (error) { | |
console.error('Error cleaning up referrals:', error); | |
} | |
} | |
/** | |
* Get the first/oldest valid referral within the attribution window | |
*/ | |
export function getApplicableReferral(): string | null { | |
try { | |
cleanupExpiredReferrals(); | |
const referrals = getStoredReferrals(); | |
if (referrals.length === 0) return null; | |
// Return the oldest (last in array) valid referral code | |
// This implements "first touch" attribution | |
return referrals[referrals.length - 1].code; | |
} catch (error) { | |
console.error('Error getting applicable referral:', error); | |
return null; | |
} | |
} | |
/** | |
* Check if a URL search param has a referral code and add it to the cookie | |
*/ | |
export function handleReferralFromURL(): void { | |
if (typeof window === 'undefined') return; | |
try { | |
const urlParams = new URLSearchParams(window.location.search); | |
const refCode = urlParams.get('ref'); | |
if (refCode) { | |
addReferral(refCode); | |
} | |
} catch (error) { | |
console.error('Error handling referral from URL:', error); | |
} | |
} | |
/** | |
* Add referral code to payment link | |
*/ | |
export function addReferralToPaymentLink(baseLink: string): string { | |
try { | |
const refCode = getApplicableReferral(); | |
if (!refCode) return baseLink; | |
// Check if URL already has parameters | |
const separator = baseLink.includes('?') ? '&' : '?'; | |
return `${baseLink}${separator}metadata_ref=${encodeURIComponent(refCode)}`; | |
} catch (error) { | |
console.error('Error adding referral to payment link:', error); | |
return baseLink; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
timestamp, | |
pgTable, | |
text, | |
numeric, | |
jsonb, | |
} from "drizzle-orm/pg-core"; | |
import { users } from "./user"; | |
// Schema to track referrals (who referred whom and commission amount) | |
export const referrals = pgTable("referrals", { | |
id: text("id") | |
.primaryKey() | |
.$defaultFn(() => crypto.randomUUID()), | |
referrerId: text("referrerId") | |
.notNull() | |
.references(() => users.id, { onDelete: "cascade" }), | |
referredId: text("referredId") | |
.notNull() | |
.references(() => users.id, { onDelete: "cascade" }), | |
referredEmail: text("referredEmail").notNull(), | |
purchaseAmount: numeric("purchaseAmount").notNull(), | |
commissionAmount: numeric("commissionAmount").notNull(), | |
planCodename: text("planCodename").notNull(), | |
status: text("status").notNull().default("pending"), // pending, approved, rejected | |
createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), | |
settledIn: text("settledIn").references(() => payouts.id), // Link to the payout this referral was settled in | |
}); | |
// Schema to track payouts to referrers | |
export const payouts = pgTable("payouts", { | |
id: text("id") | |
.primaryKey() | |
.$defaultFn(() => crypto.randomUUID()), | |
userId: text("userId") | |
.notNull() | |
.references(() => users.id, { onDelete: "cascade" }), | |
amount: numeric("amount").notNull(), | |
status: text("status").notNull().default("pending"), // pending, processing, completed, rejected | |
paymentMethod: text("paymentMethod").notNull(), // paypal, bank_transfer, etc. | |
paymentDetails: jsonb("paymentDetails"), // Store payment details like PayPal email, bank account info | |
screenshot: text("screenshot"), // URL to proof of payment screenshot | |
notes: text("notes"), | |
createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), | |
completedAt: timestamp("completedAt", { mode: "date" }), | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Referral code management | |
try { | |
const metadata = body.data.metadata || {}; | |
// Type assertion to fix TypeScript error | |
const referralCode = metadata.ref as string | undefined; | |
if (referralCode) { | |
console.log(`Referral code found: ${referralCode}`); | |
// Get the referrer user | |
const referrer = await db | |
.select() | |
.from(users) | |
.where(eq(users.referralCode, referralCode)) | |
.limit(1) | |
.then((users) => users[0]); | |
if (!referrer) { | |
console.log(`Referrer not found for code: ${referralCode}`); | |
} else if (referrer.id === user.id) { | |
// Self-referral detected | |
console.log(`Self-referral detected for user: ${user.email}`); | |
await sendMail( | |
user.email, | |
"Self-referral detected", | |
` | |
<h1>Self-referral detected</h1> | |
<p>We noticed that you used your own referral code when making a purchase. Self-referrals are not eligible for commission payouts.</p> | |
<p>If you believe this is an error, please contact our support team.</p> | |
` | |
); | |
} else { | |
const currency = body.data.currency; | |
const amountPaid = body.data.total_amount / 100; // Convert from cents to dollars | |
const taxPaid = body.data.tax / 100; // Convert from cents to dollars | |
let purchaseAmount = amountPaid - taxPaid; // Convert from cents to dollars | |
if (currency !== "USD") { | |
const response = await fetch( | |
`https://api.currencyfreaks.com/v2.0/rates/latest?apikey=b656eb60121d44c5a21cf3fdb86b91f7&symbols=${currency}` | |
); | |
// {"date":"2025-04-08 00:00:00+00","base":"USD","rates":{"INR":"85.8825"}} | |
const data = await response.json(); | |
const exchangeRate = data.rates[currency]; | |
purchaseAmount = purchaseAmount / exchangeRate; // Convert to USD | |
console.log(`Purchase amount in USD: ${purchaseAmount}`); | |
console.log(`Exchange rate: ${exchangeRate}`); | |
console.log(`Currency: ${currency}`); | |
} | |
// Valid referral - calculate commission amount (30%) | |
const commissionRate = 0.3; // 30% commission | |
const commissionAmount = purchaseAmount * commissionRate; | |
// Create a referral record | |
await db.insert(referrals).values({ | |
referrerId: referrer.id, | |
referredId: user.id, | |
referredEmail: user.email, | |
purchaseAmount: purchaseAmount.toString(), | |
commissionAmount: commissionAmount.toString(), | |
planCodename: planCodename, | |
status: "pending", // Pending approval | |
}); | |
// Send email to referrer | |
await sendMail( | |
referrer.email!, | |
"You earned a commission!", | |
` | |
<h1>You earned a commission!</h1> | |
<p>Great news! Someone just purchased Indie Kit Pro using your referral link.</p> | |
<p><strong>Purchase details:</strong></p> | |
<ul> | |
<li>Product: ${planCodename}</li> | |
<li>Purchase amount: $${purchaseAmount.toFixed(2)}</li> | |
<li>Your commission (30%): $${commissionAmount.toFixed(2)}</li> | |
</ul> | |
<p>Your commission is now pending approval and will be processed in the next payout cycle.</p> | |
<p><a href="https://indiekit.pro/app/my-referrals">View your referrals dashboard</a> to track this and other commissions.</p> | |
<p>Thank you for being part of our affiliate program!</p> | |
` | |
); | |
console.log( | |
`Referral recorded successfully for referrer: ${ | |
referrer.email | |
}, amount: $${commissionAmount.toFixed(2)}` | |
); | |
} | |
} | |
} catch (error) { | |
console.error("Error processing referral:", error); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment