diff --git a/cli/src/__tests__/helpers/mock-api-client.ts b/cli/src/__tests__/helpers/mock-api-client.ts index 720fb68dc0..fbf4423be3 100644 --- a/cli/src/__tests__/helpers/mock-api-client.ts +++ b/cli/src/__tests__/helpers/mock-api-client.ts @@ -13,7 +13,6 @@ export interface MockApiClientOverrides { usage?: ReturnType loginCode?: ReturnType loginStatus?: ReturnType - referral?: ReturnType publish?: ReturnType logout?: ReturnType feedback?: ReturnType @@ -54,8 +53,6 @@ export const createMockApiClient = ( mock(defaultOkResponse)) as CodebuffApiClient['loginCode'], loginStatus: (overrides.loginStatus ?? mock(defaultOkResponse)) as CodebuffApiClient['loginStatus'], - referral: (overrides.referral ?? - mock(defaultOkResponse)) as CodebuffApiClient['referral'], publish: (overrides.publish ?? mock(defaultOkResponse)) as CodebuffApiClient['publish'], logout: (overrides.logout ?? diff --git a/cli/src/__tests__/referral-mode.test.ts b/cli/src/__tests__/referral-mode.test.ts deleted file mode 100644 index 09607f30f5..0000000000 --- a/cli/src/__tests__/referral-mode.test.ts +++ /dev/null @@ -1,547 +0,0 @@ -import { describe, test, expect, mock } from 'bun:test' - -import { getInputModeConfig } from '../utils/input-modes' - -import type { InputMode } from '../utils/input-modes' - -// Helper type for mock functions -type MockSetInputMode = (mode: InputMode) => void - -/** - * Tests for referral mode functionality in the CLI. - * - * Referral mode is entered when user types '/referral' or '/redeem' and allows entering referral codes. - * The '◎' icon is displayed in a warning-colored column. - * - * Key behaviors: - * 1. Entering referral mode via slash commands - * 2. Input validation (3-50 alphanumeric chars with dashes) - * 3. Backspace at cursor position 0 exits referral mode - * 4. Submission auto-prefixes 'ref-' if not present - * 5. UI state changes (icon, placeholder, colors) - */ - -describe('referral-mode', () => { - describe('entering referral mode', () => { - test('typing "/referral" enters referral mode', () => { - const setInputMode = mock((_mode) => {}) - const command = '/referral' - - // Simulate command processing - if (command === '/referral' || command === '/redeem') { - setInputMode('referral') - } - - expect(setInputMode).toHaveBeenCalledWith('referral') - }) - - test('typing "/redeem" also enters referral mode', () => { - const setInputMode = mock((_mode) => {}) - const command = '/redeem' as string - - if (command === '/referral' || command === '/redeem') { - setInputMode('referral') - } - - expect(setInputMode).toHaveBeenCalledWith('referral') - }) - - test('/referral with a code argument redeems immediately without entering mode', () => { - const setInputMode = mock((_mode) => {}) - const handleReferralCode = mock(async (_code: string) => {}) - const command = '/referral abc123' - - // Simulate handler logic - const args = command.slice('/referral'.length + 1).trim() - if (args) { - // Has arguments - redeem directly - handleReferralCode('ref-abc123') - } else { - // No arguments - enter mode - setInputMode('referral') - } - - expect(handleReferralCode).toHaveBeenCalledWith('ref-abc123') - expect(setInputMode).not.toHaveBeenCalled() - }) - }) - - describe('exiting referral mode', () => { - test('backspace at cursor position 0 exits referral mode', () => { - const setInputMode = mock((_mode) => {}) - - const inputMode = 'referral' as InputMode - const cursorPosition = 0 - const key = { name: 'backspace' } - - // Simulate exit logic - if ( - inputMode !== 'default' && - cursorPosition === 0 && - key.name === 'backspace' - ) { - setInputMode('default') - } - - expect(setInputMode).toHaveBeenCalledWith('default') - }) - - test('backspace at cursor position 0 with non-empty input DOES exit referral mode', () => { - const setInputMode = mock((_mode) => {}) - - const inputMode = 'referral' as InputMode - const cursorPosition = 0 - const key = { name: 'backspace' } - - if ( - inputMode !== 'default' && - cursorPosition === 0 && - key.name === 'backspace' - ) { - setInputMode('default') - } - - // Should exit even with input, because cursor is at position 0 - expect(setInputMode).toHaveBeenCalledWith('default') - }) - - test('backspace at cursor position > 0 does NOT exit referral mode', () => { - const setInputMode = mock((_mode) => {}) - - const inputMode = 'referral' as InputMode - const cursorPosition = 5 as number - const key = { name: 'backspace' } - - if ( - inputMode !== 'default' && - cursorPosition === 0 && - key.name === 'backspace' - ) { - setInputMode('default') - } - - // Should not exit because cursor is not at position 0 - expect(setInputMode).not.toHaveBeenCalled() - }) - - test('other keys at cursor position 0 do NOT exit referral mode', () => { - const setInputMode = mock((_mode) => {}) - - const inputMode = 'referral' as InputMode - const cursorPosition = 0 - const key = { name: 'a' } - - if ( - inputMode !== 'default' && - cursorPosition === 0 && - key.name === 'backspace' - ) { - setInputMode('default') - } - - // Should not exit because key is not backspace - expect(setInputMode).not.toHaveBeenCalled() - }) - }) - - describe('referral code validation', () => { - test('valid alphanumeric code passes validation', () => { - const code = 'abc123' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(true) - }) - - test('valid code with dashes passes validation', () => { - const code = 'abc-123-xyz' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(true) - }) - - test('minimum length (3 chars) passes validation', () => { - const code = 'abc' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(true) - }) - - test('maximum length (50 chars) passes validation', () => { - const code = 'a'.repeat(50) - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(true) - }) - - test('too short (< 3 chars) fails validation', () => { - const code = 'ab' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(false) - }) - - test('too long (> 50 chars) fails validation', () => { - const code = 'a'.repeat(51) - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(false) - }) - - test('special characters fail validation', () => { - const codes = ['abc@123', 'test!code', 'ref_123', 'code.com', 'test code'] - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - codes.forEach((code) => { - expect(pattern.test(code)).toBe(false) - }) - }) - - test('empty string fails validation', () => { - const code = '' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(false) - }) - }) - - describe('referral code auto-prefixing', () => { - test('code without ref- prefix gets auto-prefixed', () => { - const userInput = 'abc123' - const referralCode = userInput.startsWith('ref-') - ? userInput - : `ref-${userInput}` - - expect(referralCode).toBe('ref-abc123') - }) - - test('code with ref- prefix stays unchanged', () => { - const userInput = 'ref-abc123' - const referralCode = userInput.startsWith('ref-') - ? userInput - : `ref-${userInput}` - - expect(referralCode).toBe('ref-abc123') - }) - - test('code with REF- (uppercase) gets normalized to lowercase prefix', () => { - const userInput = 'REF-abc123' - const userInputLower = userInput.toLowerCase() - // Normalize: case-insensitive prefix check, strip and re-add lowercase prefix - const referralCode = userInputLower.startsWith('ref-') - ? `ref-${userInput.slice(4)}` - : `ref-${userInput}` - - // Should strip REF- and re-add ref- to preserve the code portion - expect(referralCode).toBe('ref-abc123') - }) - - test('code with Ref- (mixed case) gets normalized to lowercase prefix', () => { - const userInput = 'Ref-XYZ789' - const userInputLower = userInput.toLowerCase() - const referralCode = userInputLower.startsWith('ref-') - ? `ref-${userInput.slice(4)}` - : `ref-${userInput}` - - expect(referralCode).toBe('ref-XYZ789') - }) - - test('code with rEf- (random case) gets normalized to lowercase prefix', () => { - const userInput = 'rEf-Code123' - const userInputLower = userInput.toLowerCase() - const referralCode = userInputLower.startsWith('ref-') - ? `ref-${userInput.slice(4)}` - : `ref-${userInput}` - - expect(referralCode).toBe('ref-Code123') - }) - - test('preserves code portion casing when normalizing prefix', () => { - // User typed "REF-ABC123" - should become "ref-ABC123", not "ref-abc123" - const userInput = 'REF-ABC123' - const userInputLower = userInput.toLowerCase() - const referralCode = userInputLower.startsWith('ref-') - ? `ref-${userInput.slice(4)}` - : `ref-${userInput}` - - expect(referralCode).toBe('ref-ABC123') - // Code portion should preserve original casing - expect(referralCode.slice(4)).toBe('ABC123') - }) - }) - - describe('referral mode input storage', () => { - test('input value is stored as-is without any prefix while in referral mode', () => { - const inputMode: InputMode = 'referral' - const inputValue = 'abc123' - - // The stored value should NOT have any prefix - expect(inputValue).toBe('abc123') - expect(inputValue).not.toContain('ref-') - expect(inputMode).toBe('referral') - }) - - test('user can type ref- prefix manually if desired', () => { - const inputMode: InputMode = 'referral' - const inputValue = 'ref-abc123' - - expect(inputValue).toBe('ref-abc123') - expect(inputMode).toBe('referral') - }) - }) - - describe('referral mode submission', () => { - test('submitting referral code adds ref- prefix if not present', () => { - const inputMode: InputMode = 'referral' - const trimmedInput = 'abc123' - - const referralCode = - inputMode === 'referral' - ? trimmedInput.startsWith('ref-') - ? trimmedInput - : `ref-${trimmedInput}` - : trimmedInput - - expect(referralCode).toBe('ref-abc123') - }) - - test('submitting referral code with ref- prefix keeps it', () => { - const inputMode: InputMode = 'referral' - const trimmedInput = 'ref-xyz789' - - const referralCode = - inputMode === 'referral' - ? trimmedInput.startsWith('ref-') - ? trimmedInput - : `ref-${trimmedInput}` - : trimmedInput - - expect(referralCode).toBe('ref-xyz789') - }) - - test('submission exits referral mode after processing', () => { - const setInputMode = mock((_mode) => {}) - - // After submission, referral mode should be exited - setInputMode('default') - - expect(setInputMode).toHaveBeenCalledWith('default') - }) - - test('invalid code shows error and exits referral mode', () => { - const setInputMode = mock((_mode) => {}) - const showError = mock((_msg: string) => {}) - const trimmedInput = 'ab' // Too short - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - if (!pattern.test(trimmedInput)) { - showError( - 'Invalid referral code format. Codes should be 3-50 alphanumeric characters.', - ) - setInputMode('default') - } - - expect(showError).toHaveBeenCalled() - expect(setInputMode).toHaveBeenCalledWith('default') - }) - }) - - describe('referral mode UI state', () => { - test('input mode is stored separately from input value', () => { - const state1 = { - inputMode: 'referral' as InputMode, - inputValue: 'abc123', - } - const state2 = { inputMode: 'default' as InputMode, inputValue: 'hello' } - - expect(state1.inputMode).toBe('referral') - expect(state1.inputValue).toBe('abc123') - - expect(state2.inputMode).toBe('default') - expect(state2.inputValue).toBe('hello') - }) - - test('input width is adjusted in referral mode for icon column', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.widthAdjustment).toBeGreaterThan(0) - }) - - test('input width is NOT adjusted when not in referral mode', () => { - const defaultConfig = getInputModeConfig('default') - - expect(defaultConfig.widthAdjustment).toBe(0) - }) - - test('placeholder changes in referral mode', () => { - const defaultConfig = getInputModeConfig('default') - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.placeholder).not.toBe(defaultConfig.placeholder) - }) - - test('referral mode has a placeholder', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.placeholder.length).toBeGreaterThan(0) - }) - - test('icon is displayed in referral mode', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.icon).not.toBeNull() - }) - - test('no icon is displayed in default mode', () => { - const defaultConfig = getInputModeConfig('default') - - expect(defaultConfig.icon).toBeNull() - }) - - test('border color changes to warning in referral mode', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.color).toBe('warning') - }) - - test('agent mode toggle is hidden in referral mode', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.showAgentModeToggle).toBe(false) - }) - - test('agent mode toggle is shown in default mode', () => { - const defaultConfig = getInputModeConfig('default') - - expect(defaultConfig.showAgentModeToggle).toBe(true) - }) - }) - - describe('edge cases', () => { - test('empty string is invalid referral code', () => { - const code = '' - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(code)).toBe(false) - }) - - test('whitespace is trimmed before validation', () => { - const userInput = ' abc123 ' - const trimmed = userInput.trim() - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(trimmed)).toBe(true) - }) - - test('only whitespace fails validation', () => { - const userInput = ' ' - const trimmed = userInput.trim() - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - expect(pattern.test(trimmed)).toBe(false) - }) - - test('mode can be entered, exited, and re-entered', () => { - let inputMode: InputMode = 'default' - - // Enter referral mode - inputMode = 'referral' - expect(inputMode).toBe('referral') - - // Exit referral mode - inputMode = 'default' - expect(inputMode).toBe('default') - - // Re-enter referral mode - inputMode = 'referral' - expect(inputMode).toBe('referral') - }) - - test('slash suggestions are disabled in referral mode', () => { - const referralConfig = getInputModeConfig('referral') - - expect(referralConfig.disableSlashSuggestions).toBe(true) - }) - }) - - describe('integration with command router', () => { - test('referral mode input is routed to handleReferralCode', () => { - const handleReferralCode = mock(async (_code: string) => {}) - const inputMode = 'referral' as InputMode - const trimmedInput = 'abc123' - - if (inputMode === 'referral') { - const referralCode = trimmedInput.startsWith('ref-') - ? trimmedInput - : `ref-${trimmedInput}` - handleReferralCode(referralCode) - } - - expect(handleReferralCode).toHaveBeenCalledWith('ref-abc123') - }) - - test('normal mode input is NOT routed to referral handler', () => { - const handleReferralCode = mock(async (_code: string) => {}) - const inputMode = 'default' as InputMode - const trimmedInput = 'abc123' - - if (inputMode === 'referral') { - handleReferralCode(`ref-${trimmedInput}`) - } - - expect(handleReferralCode).not.toHaveBeenCalled() - }) - - test('ref-XXXX input in default mode uses referral handler', () => { - const isReferralCode = (input: string) => { - return /^\/?ref-[a-zA-Z0-9-]{1,50}$/.test(input) - } - - const input1 = 'ref-abc123' - const input2 = '/ref-abc123' - const input3 = 'not-a-referral' - - expect(isReferralCode(input1)).toBe(true) - expect(isReferralCode(input2)).toBe(true) - expect(isReferralCode(input3)).toBe(false) - }) - }) - - describe('error handling', () => { - test('network error during redemption shows error message', async () => { - const showError = mock((_msg: string) => {}) - const handleReferralCode = mock(async (_code: string) => { - throw new Error('Network error') - }) - - try { - await handleReferralCode('ref-abc123') - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error' - showError(`Error redeeming referral code: ${errorMessage}`) - } - - expect(showError).toHaveBeenCalledWith( - 'Error redeeming referral code: Network error', - ) - }) - - test('validation error prevents redemption attempt', () => { - const handleReferralCode = mock(async (_code: string) => {}) - const showError = mock((_msg: string) => {}) - const trimmedInput = '!@#' // Invalid characters - const pattern = /^[a-zA-Z0-9-]{3,50}$/ - - if (!pattern.test(trimmedInput)) { - showError( - 'Invalid referral code format. Codes should be 3-50 alphanumeric characters.', - ) - } else { - handleReferralCode(`ref-${trimmedInput}`) - } - - expect(showError).toHaveBeenCalled() - expect(handleReferralCode).not.toHaveBeenCalled() - }) - }) -}) diff --git a/cli/src/commands/__tests__/command-args.test.ts b/cli/src/commands/__tests__/command-args.test.ts index 63047c1230..f20a1d4810 100644 --- a/cli/src/commands/__tests__/command-args.test.ts +++ b/cli/src/commands/__tests__/command-args.test.ts @@ -176,7 +176,6 @@ describe('command factory pattern', () => { const expectedWithArgs = [ 'feedback', 'bash', - 'refer-friends', 'image', 'publish', 'new', diff --git a/cli/src/commands/__tests__/router-input.test.ts b/cli/src/commands/__tests__/router-input.test.ts index 653063abbc..c4589477b1 100644 --- a/cli/src/commands/__tests__/router-input.test.ts +++ b/cli/src/commands/__tests__/router-input.test.ts @@ -3,51 +3,12 @@ import { describe, test, expect } from 'bun:test' import { SLASH_COMMANDS } from '../../data/slash-commands' import { findCommand, COMMAND_REGISTRY } from '../command-registry' import { - normalizeInput, parseCommand, isSlashCommand, - isReferralCode, parseCommandInput, } from '../router-utils' describe('router-utils', () => { - describe('normalizeInput', () => { - test('strips leading slash from input', () => { - expect(normalizeInput('/help')).toBe('help') - expect(normalizeInput('/logout')).toBe('logout') - expect(normalizeInput('/ref-abc123')).toBe('ref-abc123') - }) - - test('preserves input without leading slash', () => { - expect(normalizeInput('help')).toBe('help') - expect(normalizeInput('ref-abc123')).toBe('ref-abc123') - expect(normalizeInput('some prompt text')).toBe('some prompt text') - }) - - test('handles empty string', () => { - expect(normalizeInput('')).toBe('') - }) - - test('handles only slash', () => { - expect(normalizeInput('/')).toBe('') - }) - - test('handles multiple slashes', () => { - expect(normalizeInput('//help')).toBe('/help') - expect(normalizeInput('///test')).toBe('//test') - }) - - test('preserves internal slashes', () => { - expect(normalizeInput('/path/to/file')).toBe('path/to/file') - expect(normalizeInput('path/to/file')).toBe('path/to/file') - }) - - test('preserves whitespace in input', () => { - expect(normalizeInput('/help me')).toBe('help me') - expect(normalizeInput('help me')).toBe('help me') - }) - }) - describe('isSlashCommand', () => { test('returns true for input starting with /', () => { expect(isSlashCommand('/help')).toBe(true) @@ -111,34 +72,6 @@ describe('router-utils', () => { }) }) - describe('isReferralCode', () => { - test('recognizes referral codes with slash prefix', () => { - expect(isReferralCode('/ref-abc123')).toBe(true) - expect(isReferralCode('/ref-XYZ')).toBe(true) - expect(isReferralCode('/ref-')).toBe(true) - }) - - test('recognizes referral codes without slash prefix', () => { - expect(isReferralCode('ref-abc123')).toBe(true) - expect(isReferralCode('ref-XYZ')).toBe(true) - expect(isReferralCode('ref-')).toBe(true) - }) - - test('rejects inputs that are not referral codes', () => { - expect(isReferralCode('reference')).toBe(false) - expect(isReferralCode('refund')).toBe(false) - expect(isReferralCode('/reference')).toBe(false) - expect(isReferralCode('ref abc')).toBe(false) - expect(isReferralCode('')).toBe(false) - }) - - test('is case-sensitive for ref- prefix', () => { - expect(isReferralCode('REF-abc')).toBe(false) - expect(isReferralCode('Ref-abc')).toBe(false) - expect(isReferralCode('/REF-abc')).toBe(false) - }) - }) - describe('parseCommandInput', () => { test('returns command info for exact slashless matches', () => { expect(parseCommandInput('init')).toEqual({ @@ -258,41 +191,6 @@ describe('router-utils', () => { } }) - describe('referral code detection with different input formats', () => { - const validCodes = [ - 'ref-abc123', - '/ref-abc123', - 'ref-TEST', - '/ref-TEST', - 'ref-12345', - '/ref-12345', - ] - - const invalidCodes = [ - 'reference', - '/reference', - 'refund-123', - '/refund-123', - 'REF-abc', - '/REF-abc', - 'ref abc', - '/ref abc', - '', - '/', - ] - - for (const code of validCodes) { - test(`recognizes "${code}" as valid referral code`, () => { - expect(isReferralCode(code)).toBe(true) - }) - } - - for (const code of invalidCodes) { - test(`rejects "${code}" as referral code`, () => { - expect(isReferralCode(code)).toBe(false) - }) - } - }) }) describe('command-registry', () => { diff --git a/cli/src/commands/command-registry.ts b/cli/src/commands/command-registry.ts index b44451f54a..8b6c431baf 100644 --- a/cli/src/commands/command-registry.ts +++ b/cli/src/commands/command-registry.ts @@ -8,9 +8,7 @@ import { useThemeStore } from '../hooks/use-theme' import { handleHelpCommand } from './help' import { handleImageCommand } from './image' import { handleInitializationFlowLocally } from './init' -import { handleReferralCode } from './referral' import { runBashCommand } from './router' -import { normalizeReferralCode } from './router-utils' import { handleUsageCommand } from './usage' import { WEBSITE_URL } from '../login/constants' import { useChatStore } from '../state/chat-store' @@ -169,7 +167,6 @@ const clearInput = (params: RouterParams) => { const FREEBUFF_REMOVED_COMMANDS = new Set([ 'ads:enable', 'ads:disable', - 'refer-friends', 'usage', 'subscribe', 'image', @@ -250,42 +247,6 @@ const ALL_COMMANDS: CommandDefinition[] = [ clearInput(params) }, }), - defineCommandWithArgs({ - name: 'refer-friends', - aliases: ['referral', 'redeem'], - handler: async (params, args) => { - const trimmedArgs = args.trim() - - // If user provided a code directly, redeem it immediately - if (trimmedArgs) { - const code = normalizeReferralCode(trimmedArgs) - try { - const { postUserMessage } = await handleReferralCode(code) - params.setMessages((prev) => [ - ...prev, - getUserMessage(params.inputValue.trim()), - ...postUserMessage([]), - ]) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error' - params.setMessages((prev) => [ - ...prev, - getUserMessage(params.inputValue.trim()), - getSystemMessage(`Error redeeming referral code: ${errorMessage}`), - ]) - } - params.saveToHistory(params.inputValue.trim()) - clearInput(params) - return - } - - // Otherwise enter referral mode - useChatStore.getState().setInputMode('referral') - params.saveToHistory(params.inputValue.trim()) - clearInput(params) - }, - }), defineCommand({ name: 'login', aliases: ['signin'], diff --git a/cli/src/commands/referral.ts b/cli/src/commands/referral.ts deleted file mode 100644 index 4f2067f0e8..0000000000 --- a/cli/src/commands/referral.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { env } from '@codebuff/common/env' -import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants' - -import { getAuthToken } from '../utils/auth' -import { getApiClient, setApiClientAuthToken } from '../utils/codebuff-api' -import { logger } from '../utils/logger' -import { getSystemMessage } from '../utils/message-history' - -import type { PostUserMessageFn } from '../types/contracts/send-message' - -export async function handleReferralCode(referralCode: string): Promise<{ - postUserMessage: PostUserMessageFn -}> { - const authToken = getAuthToken() - - if (!authToken) { - const postUserMessage: PostUserMessageFn = (prev) => [ - ...prev, - getSystemMessage( - 'Please log in first to redeem a referral code. Use /login to authenticate.', - ), - ] - return { postUserMessage } - } - - setApiClientAuthToken(authToken) - const apiClient = getApiClient() - - try { - const response = await apiClient.referral({ referralCode }) - - if (!response.ok) { - const errorMessage = response.error ?? 'Failed to redeem referral code' - logger.error( - { - referralCode, - error: errorMessage, - }, - 'Error redeeming referral code', - ) - const postUserMessage: PostUserMessageFn = (prev) => [ - ...prev, - getSystemMessage(`Error: ${errorMessage}`), - ] - return { postUserMessage } - } - - const creditsRedeemed = - response.data?.credits_redeemed ?? CREDITS_REFERRAL_BONUS - const postUserMessage: PostUserMessageFn = (prev) => [ - ...prev, - getSystemMessage( - `🎉 Noice, you've earned an extra ${creditsRedeemed} credits!\n\n` + - `(pssst: you can also refer new users and earn ${CREDITS_REFERRAL_BONUS} credits for each referral at: ${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/referrals)`, - ), - ] - return { postUserMessage } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - logger.error( - { - referralCode, - error: errorMessage, - }, - 'Error redeeming referral code', - ) - const postUserMessage: PostUserMessageFn = (prev) => [ - ...prev, - getSystemMessage(`Error redeeming referral code: ${errorMessage}`), - ] - return { postUserMessage } - } -} diff --git a/cli/src/commands/router-utils.ts b/cli/src/commands/router-utils.ts index 02a3341c27..069b22304b 100644 --- a/cli/src/commands/router-utils.ts +++ b/cli/src/commands/router-utils.ts @@ -1,25 +1,11 @@ import { SLASHLESS_COMMAND_IDS } from '../data/slash-commands' -/** - * Normalize user input by stripping the leading slash if present. - * This is used for referral codes which work with or without the slash. - * - * @example - * normalizeInput('/help') // => 'help' - * normalizeInput('help') // => 'help' - * normalizeInput('/ref-abc123') // => 'ref-abc123' - */ -export function normalizeInput(input: string): string { - return input.startsWith('/') ? input.slice(1) : input -} - /** * Check if the input is a slash command (starts with '/'). * * @example * isSlashCommand('/help') // => true * isSlashCommand('help') // => false - * isSlashCommand('/ref-abc123') // => true */ export function isSlashCommand(input: string): boolean { return input.trim().startsWith('/') @@ -47,54 +33,6 @@ export function parseCommand(input: string): string { return firstWord.toLowerCase() } -/** - * Check if the input is a referral code (starts with 'ref-'). - * Works with or without the leading slash. - * - * @example - * isReferralCode('ref-abc123') // => true - * isReferralCode('/ref-abc123') // => true - * isReferralCode('reference') // => false - */ -export function isReferralCode(input: string): boolean { - const normalized = normalizeInput(input.trim()) - return normalized.startsWith('ref-') -} - -/** - * Extract the referral code from user input. - * Returns the normalized code without the leading slash. - * - * @example - * extractReferralCode('/ref-abc123') // => 'ref-abc123' - * extractReferralCode('ref-abc123') // => 'ref-abc123' - */ -export function extractReferralCode(input: string): string { - return normalizeInput(input.trim()) -} - -const REFERRAL_PREFIX = 'ref-' - -/** - * Normalize a referral code by ensuring it has the lowercase 'ref-' prefix. - * Handles case-insensitive prefix detection (REF-, Ref-, etc.) and preserves - * the original casing of the code portion. - * - * @example - * normalizeReferralCode('abc123') // => 'ref-abc123' - * normalizeReferralCode('ref-abc123') // => 'ref-abc123' - * normalizeReferralCode('REF-ABC123') // => 'ref-ABC123' - * normalizeReferralCode('Ref-XYZ') // => 'ref-XYZ' - */ -export function normalizeReferralCode(code: string): string { - const trimmed = code.trim() - const hasPrefix = trimmed.toLowerCase().startsWith(REFERRAL_PREFIX) - const codeWithoutPrefix = hasPrefix - ? trimmed.slice(REFERRAL_PREFIX.length) - : trimmed - return `${REFERRAL_PREFIX}${codeWithoutPrefix}` -} - /** * Result of parsing a command-like input. */ diff --git a/cli/src/commands/router.ts b/cli/src/commands/router.ts index b0c8b9915c..7a67988459 100644 --- a/cli/src/commands/router.ts +++ b/cli/src/commands/router.ts @@ -9,12 +9,8 @@ import { type RouterParams, type CommandResult, } from './command-registry' -import { handleReferralCode } from './referral' import { isSlashCommand, - isReferralCode, - extractReferralCode, - normalizeReferralCode, parseCommandInput, } from './router-utils' import { handleClaudeAuthCode } from '../components/claude-connect-banner' @@ -435,70 +431,6 @@ export async function routeUserPrompt( return } - // Handle referral mode input - if (inputMode === 'referral') { - // Validate the referral code (3-50 alphanumeric chars with optional dashes) - const codePattern = /^[a-zA-Z0-9-]{3,50}$/ - // Strip prefix if present for validation (case-insensitive) - const codeWithoutPrefix = trimmed.toLowerCase().startsWith('ref-') - ? trimmed.slice(4) - : trimmed - - if (!codePattern.test(codeWithoutPrefix)) { - setMessages((prev) => [ - ...prev, - getUserMessage(trimmed), - getSystemMessage( - 'Invalid referral code format. Codes should be 3-50 alphanumeric characters.', - ), - ]) - saveToHistory(trimmed) - setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }) - setInputMode('default') - return - } - - const referralCode = normalizeReferralCode(trimmed) - try { - const { postUserMessage: referralPostMessage } = - await handleReferralCode(referralCode) - setMessages((prev) => [ - ...prev, - getUserMessage(trimmed), - ...referralPostMessage([]), - ]) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Unknown error' - setMessages((prev) => [ - ...prev, - getUserMessage(trimmed), - getSystemMessage(`Error redeeming referral code: ${errorMessage}`), - ]) - } - saveToHistory(trimmed) - setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }) - setInputMode('default') - - return - } - - // Handle referral codes (ref-XXXX format) - // Works with or without leading slash: "ref-123" or "/ref-123" - if (isReferralCode(trimmed)) { - const referralCode = extractReferralCode(trimmed) - const { postUserMessage: referralPostMessage } = - await handleReferralCode(referralCode) - setMessages((prev) => [ - ...prev, - getUserMessage(trimmed), - ...referralPostMessage([]), - ]) - saveToHistory(trimmed) - setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false }) - return - } - // Handle slash commands or configured slashless exact commands. const parsedCommand = parseCommandInput(trimmed) if (parsedCommand) { diff --git a/cli/src/components/chat-input-bar.tsx b/cli/src/components/chat-input-bar.tsx index 5241d558f2..cee0a296eb 100644 --- a/cli/src/components/chat-input-bar.tsx +++ b/cli/src/components/chat-input-bar.tsx @@ -199,11 +199,6 @@ export const ChatInputBar = ({ return } - // Referral mode: show only the referral banner (no input box) - if (inputMode === 'referral') { - return - } - // ChatGPT connect mode: show only the connect panel (no input box) if (inputMode === 'connect:chatgpt') { return diff --git a/cli/src/components/input-mode-banner.tsx b/cli/src/components/input-mode-banner.tsx index 66335245ba..be0d2df8ca 100644 --- a/cli/src/components/input-mode-banner.tsx +++ b/cli/src/components/input-mode-banner.tsx @@ -7,7 +7,6 @@ import { ChatGptConnectBanner } from './chatgpt-connect-banner' import { ClaudeConnectBanner } from './claude-connect-banner' import { HelpBanner } from './help-banner' import { PendingAttachmentsBanner } from './pending-attachments-banner' -import { ReferralBanner } from './referral-banner' import { SubscriptionLimitBanner } from './subscription-limit-banner' import { UsageBanner } from './usage-banner' import { useChatStore } from '../state/chat-store' @@ -28,7 +27,6 @@ const BANNER_REGISTRY: Record< default: () => , image: () => , ...(IS_FREEBUFF ? {} : { usage: ({ showTime }: { showTime: number }) => }), - ...(IS_FREEBUFF ? {} : { referral: () => }), help: () => , ...(CLAUDE_OAUTH_ENABLED && !IS_FREEBUFF ? { 'connect:claude': () => } diff --git a/cli/src/components/referral-banner.tsx b/cli/src/components/referral-banner.tsx deleted file mode 100644 index e46c0272e9..0000000000 --- a/cli/src/components/referral-banner.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants' -import { WEBSITE_URL } from '@codebuff/sdk' -import { useQuery } from '@tanstack/react-query' -import React, { useState } from 'react' - -import { BottomBanner } from './bottom-banner' -import { Button } from './button' -import { useChatStore } from '../state/chat-store' -import { useTheme } from '../hooks/use-theme' -import { useTimeout } from '../hooks/use-timeout' -import { getAuthToken } from '../utils/auth' -import { getApiClient } from '../utils/codebuff-api' -import { copyTextToClipboard } from '../utils/clipboard' -import { BORDER_CHARS } from '../utils/ui-constants' - -interface ReferralData { - referralCode: string - referrals: { id: string }[] - referralLimit: number -} - -export const ReferralBanner = () => { - const setInputMode = useChatStore((state) => state.setInputMode) - const theme = useTheme() - const [isHovered, setIsHovered] = useState(false) - const [isCopied, setIsCopied] = useState(false) - const { setTimeout } = useTimeout() - const authToken = getAuthToken() - - const { data: referralData } = useQuery({ - queryKey: ['referrals'], - queryFn: async () => { - const client = getApiClient() - const response = await client.get('/api/referrals', { - includeCookie: true, - }) - if (!response.ok) { - throw new Error(`Failed to fetch referral data: ${response.status}`) - } - return response.data! - }, - enabled: !!authToken, - staleTime: 5 * 60 * 1000, - retry: false, - }) - - const referralCode = referralData?.referralCode ?? null - const referralLink = referralCode ? `${WEBSITE_URL}/referrals/${referralCode}` : null - const referralCount = referralData?.referrals.length ?? null - const referralLimit = referralData?.referralLimit ?? null - - const handleCopy = async () => { - if (!referralLink) return - try { - await copyTextToClipboard(referralLink, { suppressGlobalMessage: true }) - setIsCopied(true) - setTimeout('reset-copied', () => setIsCopied(false), 2000) - } catch { - // Error is already logged and displayed by copyTextToClipboard - } - } - - const copyLabel = isCopied ? '✔ Copied!' : '⎘ Copy referral link' - - return ( - setInputMode('default')} - > - - - {`Share this link with friends and you'll both earn ${CREDITS_REFERRAL_BONUS} credits`} - - - {referralCount !== null && referralLimit !== null && ( - - {`You've referred ${referralCount}/${referralLimit} people`} - - )} - - {referralLink ? ( - - {referralLink} - - - - - ) : ( - Loading referral link... - )} - - - ) -} diff --git a/cli/src/data/slash-commands.ts b/cli/src/data/slash-commands.ts index 4550895846..bd67811d32 100644 --- a/cli/src/data/slash-commands.ts +++ b/cli/src/data/slash-commands.ts @@ -2,7 +2,6 @@ import { CHATGPT_OAUTH_ENABLED } from '@codebuff/common/constants/chatgpt-oauth' import { CLAUDE_OAUTH_ENABLED } from '@codebuff/common/constants/claude-oauth' import { AGENT_MODES, IS_FREEBUFF } from '../utils/constants' import { getChatGptOAuthStatus } from '../utils/chatgpt-oauth' -import { CREDITS_REFERRAL_BONUS } from '@codebuff/common/old-constants' import type { SkillsMap } from '@codebuff/common/types/skill' @@ -37,7 +36,6 @@ const FREEBUFF_REMOVED_COMMAND_IDS = new Set([ 'connect:claude', 'ads:enable', 'ads:disable', - 'refer-friends', 'usage', 'subscribe', 'agent:gpt-5', @@ -90,12 +88,6 @@ const ALL_SLASH_COMMANDS: SlashCommand[] = [ label: 'ads:disable', description: 'Disable contextual ads', }, - { - id: 'refer-friends', - label: 'refer-friends', - description: `Refer friends for ${CREDITS_REFERRAL_BONUS} bonus credits each`, - aliases: ['referral'], - }, { id: 'init', label: 'init', diff --git a/cli/src/hooks/__tests__/use-user-details-query.test.ts b/cli/src/hooks/__tests__/use-user-details-query.test.ts index 77530dc01d..1dcdaae4e5 100644 --- a/cli/src/hooks/__tests__/use-user-details-query.test.ts +++ b/cli/src/hooks/__tests__/use-user-details-query.test.ts @@ -162,29 +162,6 @@ describe('fetchUserDetails', () => { expect(result).toEqual(mockUserDetails) }) - test('returns null referral_code when not set', async () => { - const mockUserDetails = { - referral_code: null, - } - - const meMock = mock(() => - Promise.resolve({ - ok: true, - status: 200, - data: mockUserDetails, - }), - ) - const apiClient = createMockApiClient({ me: meMock }) - - const result = await fetchUserDetails({ - authToken: 'valid-token', - fields: ['referral_code'] as const, - logger: mockLogger, - apiClient, - }) - - expect(result?.referral_code).toBe(null) - }) }) describe('environment validation', () => { diff --git a/cli/src/hooks/use-chat-keyboard.ts b/cli/src/hooks/use-chat-keyboard.ts index a7ef9feb2f..a2cc87daf9 100644 --- a/cli/src/hooks/use-chat-keyboard.ts +++ b/cli/src/hooks/use-chat-keyboard.ts @@ -276,7 +276,7 @@ function dispatchAction( * Integrates priority-based action resolution with handlers. * * This hook handles: - * - Mode switching (bash, referral, etc.) + * - Mode switching (bash, etc.) * - Stream interruption * - Suggestion menu navigation (slash and mention menus) * - History navigation diff --git a/cli/src/utils/__tests__/fetch-usage.test.ts b/cli/src/utils/__tests__/fetch-usage.test.ts index d7a0c854c9..1b2e68f6e6 100644 --- a/cli/src/utils/__tests__/fetch-usage.test.ts +++ b/cli/src/utils/__tests__/fetch-usage.test.ts @@ -44,9 +44,6 @@ describe('fetchAndUpdateUsage (deprecated)', () => { loginStatus: mock(() => Promise.resolve({ ok: true, status: 200, data: {} }), ) as CodebuffApiClient['loginStatus'], - referral: mock(() => - Promise.resolve({ ok: true, status: 200, data: {} }), - ) as CodebuffApiClient['referral'], publish: mock(() => Promise.resolve({ ok: true, status: 200, data: {} }), ) as CodebuffApiClient['publish'], diff --git a/cli/src/utils/__tests__/keyboard-actions.test.ts b/cli/src/utils/__tests__/keyboard-actions.test.ts index 75332053dc..c518b47ea7 100644 --- a/cli/src/utils/__tests__/keyboard-actions.test.ts +++ b/cli/src/utils/__tests__/keyboard-actions.test.ts @@ -54,17 +54,6 @@ describe('resolveChatKeyboardAction', () => { }) }) - test('escape in referral mode exits mode even while streaming', () => { - const state: ChatKeyboardState = { - ...defaultState, - inputMode: 'referral', - isStreaming: true, - } - expect(resolveChatKeyboardAction(escapeKey, state)).toEqual({ - type: 'exit-input-mode', - }) - }) - test('escape in usage mode exits mode', () => { const state: ChatKeyboardState = { ...defaultState, diff --git a/cli/src/utils/codebuff-api.ts b/cli/src/utils/codebuff-api.ts index f4266af029..75a14c6598 100644 --- a/cli/src/utils/codebuff-api.ts +++ b/cli/src/utils/codebuff-api.ts @@ -20,10 +20,10 @@ export type ApiResponse = // ============================================================================ /** User fields that can be fetched from /api/v1/me */ -export type UserField = 'id' | 'email' | 'discord_id' | 'referral_code' +export type UserField = 'id' | 'email' | 'discord_id' export type UserDetails = { - [K in T]: K extends 'discord_id' | 'referral_code' ? string | null : string + [K in T]: K extends 'discord_id' ? string | null : string } export interface UsageRequest { @@ -58,15 +58,6 @@ export interface LoginStatusResponse { user?: Record } -export interface ReferralRequest { - referralCode: string -} - -export interface ReferralResponse { - credits_redeemed?: number - error?: string -} - export interface LogoutRequest { userId?: string fingerprintId?: string @@ -191,9 +182,6 @@ export interface CodebuffApiClient { req: LoginStatusRequest, ): Promise> - /** Redeem a referral code via /api/referrals */ - referral(req: ReferralRequest): Promise> - /** Publish agents via /api/agents/publish */ publish( data: Record[], @@ -496,17 +484,6 @@ export function createCodebuffApiClient( }) }, - referral(req: ReferralRequest): Promise> { - // Auth is sent via Authorization header (includeAuth defaults to true) - // Also include cookie for legacy web session support - return request( - 'POST', - '/api/referrals', - { referralCode: req.referralCode }, - { includeCookie: true }, - ) - }, - publish( data: Record[], allLocalAgentIds?: string[], diff --git a/cli/src/utils/input-modes.ts b/cli/src/utils/input-modes.ts index 3b96ded5bf..2c6d921948 100644 --- a/cli/src/utils/input-modes.ts +++ b/cli/src/utils/input-modes.ts @@ -12,7 +12,6 @@ export type InputMode = | 'plan' | 'review' | 'interview' - | 'referral' | 'usage' | 'image' | 'help' @@ -113,16 +112,6 @@ export const INPUT_MODE_CONFIGS: Record = { disableSlashSuggestions: true, blockKeyboardExit: false, }, - referral: { - icon: '◎', - label: null, - color: 'warning', - placeholder: 'have a code? enter it here', - widthAdjustment: 2, // 1 char + 1 padding - showAgentModeToggle: false, - disableSlashSuggestions: true, - blockKeyboardExit: false, - }, usage: { icon: null, label: null, diff --git a/common/src/constants/analytics-events.ts b/common/src/constants/analytics-events.ts index acbcd190e8..5df0f2809d 100644 --- a/common/src/constants/analytics-events.ts +++ b/common/src/constants/analytics-events.ts @@ -45,7 +45,6 @@ export enum AnalyticsEvent { // Web - Authentication AUTH_LOGIN_STARTED = 'auth.login_started', - AUTH_REFERRAL_GITHUB_LOGIN_STARTED = 'auth.referral_github_login_started', AUTH_LOGOUT_COMPLETED = 'auth.logout_completed', // Web - Cookie Consent @@ -63,6 +62,9 @@ export enum AnalyticsEvent { ONBOARD_PAGE_RUN_COMMAND_COPIED = 'onboard_page.run_command_copied', ONBOARD_PAGE_INSTALL_COMMAND_COPIED = 'onboard_page.install_command_copied', + // Web - Creator Attribution + CODEBUFF_REFERRER_ATTRIBUTED = 'codebuff.referrer_attributed', + // Web - Install Dialog INSTALL_DIALOG_CD_COMMAND_COPIED = 'install_dialog.cd_command_copied', INSTALL_DIALOG_RUN_COMMAND_COPIED = 'install_dialog.run_command_copied', @@ -87,7 +89,6 @@ export enum AnalyticsEvent { // Web - UI Components TOAST_SHOWN = 'toast.shown', - REFERRAL_BANNER_CLICKED = 'referral_banner.clicked', // Web - API AGENT_RUN_API_REQUEST = 'api.agent_run_request', @@ -147,7 +148,7 @@ export enum AnalyticsEvent { CHATGPT_OAUTH_RATE_LIMITED = 'sdk.chatgpt_oauth_rate_limited', CHATGPT_OAUTH_AUTH_ERROR = 'sdk.chatgpt_oauth_auth_error', - // Freebuff - Referral Attribution + // Freebuff - Creator Attribution FREEBUFF_REFERRER_ATTRIBUTED = 'freebuff.referrer_attributed', // Freebuff - Get Started Page diff --git a/common/src/constants/limits.ts b/common/src/constants/limits.ts index e887c16aa7..515eaa4adc 100644 --- a/common/src/constants/limits.ts +++ b/common/src/constants/limits.ts @@ -5,14 +5,6 @@ export const MAX_DATE = new Date(86399999999999) export const BILLING_PERIOD_DAYS = 30 export const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 // 30 days export const SESSION_TIME_WINDOW_MS = 30 * 60 * 1000 // 30 minutes - used for matching sessions created around fingerprint creation -// Referral credits disabled 2026-04-17: setting bonus to 0 stops new referral credit grants -// without removing the referral-tracking records. See scripts/opus-or-bleed.ts for the -// abuse pattern that motivated this (self-referral rings farming 1000 free credits per -// signup and burning them on Opus). Development focus is shifting to freebuff which has -// no credit system, so we don't need this growth lever going forward. -export const CREDITS_REFERRAL_BONUS = 0 -export const AFFILIATE_USER_REFFERAL_LIMIT = 500 - // Default number of free credits granted per cycle export const DEFAULT_FREE_CREDITS_GRANT = 500 diff --git a/common/src/testing/fixtures/agent-runtime.ts b/common/src/testing/fixtures/agent-runtime.ts index 75c555de86..f4d1430127 100644 --- a/common/src/testing/fixtures/agent-runtime.ts +++ b/common/src/testing/fixtures/agent-runtime.ts @@ -111,7 +111,6 @@ export const TEST_AGENT_RUNTIME_IMPL = Object.freeze({ id: 'test-user-id', email: 'test@example.com', discord_id: 'test-discord-id', - referral_code: 'ref-test-code', stripe_customer_id: null, banned: false, created_at: new Date('2024-01-01T00:00:00Z'), diff --git a/common/src/types/contracts/database.ts b/common/src/types/contracts/database.ts index d95ba17d84..88685c7205 100644 --- a/common/src/types/contracts/database.ts +++ b/common/src/types/contracts/database.ts @@ -5,7 +5,6 @@ type User = { id: string email: string discord_id: string | null - referral_code: string | null stripe_customer_id: string | null banned: boolean created_at: Date @@ -14,7 +13,6 @@ export const userColumns = [ 'id', 'email', 'discord_id', - 'referral_code', 'stripe_customer_id', 'banned', 'created_at', diff --git a/common/src/util/referral.ts b/common/src/util/referral.ts deleted file mode 100644 index 940ba4a10f..0000000000 --- a/common/src/util/referral.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { env } from '@codebuff/common/env' - -export const getReferralLink = (referralCode: string): string => - `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/referrals/${referralCode}` diff --git a/freebuff/web/src/app/api/auth/cli/code/route.ts b/freebuff/web/src/app/api/auth/cli/code/route.ts index 8dcbca2e5c..ac7ac073c6 100644 --- a/freebuff/web/src/app/api/auth/cli/code/route.ts +++ b/freebuff/web/src/app/api/auth/cli/code/route.ts @@ -11,7 +11,6 @@ import { logger } from '@/util/logger' export async function POST(req: Request) { const reqSchema = z.object({ fingerprintId: z.string(), - referralCode: z.string().optional(), }) const requestBody = await req.json() const result = reqSchema.safeParse(requestBody) @@ -19,7 +18,7 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }) } - const { fingerprintId, referralCode } = result.data + const { fingerprintId } = result.data try { const expiresAt = Date.now() + 60 * 60 * 1000 // 1 hour @@ -54,9 +53,7 @@ export async function POST(req: Request) { ) } - const loginUrl = `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}${ - referralCode ? `&referral_code=${referralCode}` : '' - }` + const loginUrl = `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}` return NextResponse.json({ fingerprintId, diff --git a/freebuff/web/src/app/layout.tsx b/freebuff/web/src/app/layout.tsx index d3460e6374..5b753be959 100644 --- a/freebuff/web/src/app/layout.tsx +++ b/freebuff/web/src/app/layout.tsx @@ -3,6 +3,7 @@ import '@/styles/globals.css' import type { Metadata } from 'next' import { Footer } from '@/components/footer' +import { ReferrerTracker } from '@/components/referrer-tracker' import { ThemeProvider } from '@/components/theme-provider' import { siteConfig } from '@/lib/constant' import { fonts } from '@/lib/fonts' @@ -55,6 +56,7 @@ export default function RootLayout({ +
{children}