From 9816201adc8c7985aa40a5dc7d02635db2ff30ef Mon Sep 17 00:00:00 2001 From: Clawd Vader Date: Mon, 11 May 2026 15:05:25 +0200 Subject: [PATCH] feat: support template referral codes --- packages/sdk/echo-start/src/index.ts | 72 ++++++++++++++++++- .../src/hooks/useRegisterReferralCode.ts | 50 ++++++++++--- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/packages/sdk/echo-start/src/index.ts b/packages/sdk/echo-start/src/index.ts index c249d50d1..72bf51493 100644 --- a/packages/sdk/echo-start/src/index.ts +++ b/packages/sdk/echo-start/src/index.ts @@ -14,7 +14,7 @@ import chalk from 'chalk'; import { spawn } from 'child_process'; import { Command } from 'commander'; import degit from 'degit'; -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import path from 'path'; const program = new Command(); @@ -202,6 +202,76 @@ function resolveTemplateRepo(template: string): string { return repo; } + +interface EchoTemplateConfig { + referralCode?: unknown; + referral_code?: unknown; +} + +const SAFE_REFERRAL_CODE_PATTERN = /^[a-zA-Z0-9_\-.]+$/; + +function sanitizeReferralCode(code: unknown): string | null { + if (typeof code !== 'string') return null; + const trimmed = code.trim(); + if (!trimmed || trimmed.length > 128) return null; + if (!SAFE_REFERRAL_CODE_PATTERN.test(trimmed)) return null; + return trimmed; +} + +function readTemplateReferralCode(projectPath: string): string | null { + const configPath = path.join(projectPath, 'echo.config.json'); + if (!existsSync(configPath)) return null; + + try { + const config = JSON.parse( + readFileSync(configPath, 'utf-8') + ) as EchoTemplateConfig; + return sanitizeReferralCode(config.referralCode ?? config.referral_code); + } catch { + log.warning('Could not parse echo.config.json referral metadata'); + return null; + } +} + +function detectReferralEnvVarName(projectPath: string): string { + const appIdEnvVar = detectEnvVarName(projectPath) ?? detectFrameworkEnvVarName(projectPath); + + if (appIdEnvVar.startsWith('NEXT_PUBLIC_')) { + return 'NEXT_PUBLIC_ECHO_REFERRAL_CODE'; + } + if (appIdEnvVar.startsWith('VITE_')) { + return 'VITE_ECHO_REFERRAL_CODE'; + } + if (appIdEnvVar.startsWith('REACT_APP_')) { + return 'REACT_APP_ECHO_REFERRAL_CODE'; + } + + return 'ECHO_REFERRAL_CODE'; +} + +function upsertEnvVar(envPath: string, name: string, value: string): void { + const line = `${name}=${value}`; + if (!existsSync(envPath)) { + writeFileSync(envPath, `${line}\n`); + return; + } + + const current = readFileSync(envPath, 'utf-8'); + const pattern = new RegExp(`^${name}\\s*=.*$`, 'm'); + const updated = pattern.test(current) + ? current.replace(pattern, line) + : `${current.trimEnd()}\n${line}\n`; + + writeFileSync(envPath, updated); +} + +function removeTemplateConfig(projectPath: string): void { + const configPath = path.join(projectPath, 'echo.config.json'); + if (existsSync(configPath)) { + unlinkSync(configPath); + } +} + function detectEnvVarName(projectPath: string): string | null { const envFiles = ['.env.local', '.env.example', '.env']; diff --git a/packages/sdk/react/src/hooks/useRegisterReferralCode.ts b/packages/sdk/react/src/hooks/useRegisterReferralCode.ts index d5e26e5f1..78b5e52e9 100644 --- a/packages/sdk/react/src/hooks/useRegisterReferralCode.ts +++ b/packages/sdk/react/src/hooks/useRegisterReferralCode.ts @@ -8,9 +8,43 @@ interface UseRegisterReferralCodeOptions { onError?: (error: string) => void; } +function readPublicEnv(name: string): string | undefined { + const maybeGlobal = globalThis as unknown as { + import?: { meta?: { env?: Record } }; + process?: { env?: Record }; + }; + + return maybeGlobal.import?.meta?.env?.[name] || maybeGlobal.process?.env?.[name]; +} + +function getConfiguredReferralCode(): string | null { + if (typeof window === 'undefined') return null; + + const urlParams = new URLSearchParams(window.location.search); + return ( + urlParams.get('referral_code') || + urlParams.get('referralCode') || + readPublicEnv('VITE_ECHO_REFERRAL_CODE') || + readPublicEnv('NEXT_PUBLIC_ECHO_REFERRAL_CODE') || + readPublicEnv('REACT_APP_ECHO_REFERRAL_CODE') || + null + ); +} + +function removeReferralParamsFromUrl(): void { + const urlParams = new URLSearchParams(window.location.search); + urlParams.delete('referral_code'); + urlParams.delete('referralCode'); + window.history.replaceState( + {}, + '', + `${window.location.pathname}${urlParams.toString() ? `?${urlParams.toString()}` : ''}` + ); +} + /** - * Custom hook to handle referral code registration from URL parameters. - * Automatically checks for referralCode parameter in the URL and registers it for the given app. + * Custom hook to handle referral code registration from URL parameters or + * framework-public environment variables injected by echo-start external templates. */ export function useRegisterReferralCode({ appId, @@ -22,8 +56,7 @@ export function useRegisterReferralCode({ const registerReferralCode = async () => { if (typeof window === 'undefined') return; - const urlParams = new URLSearchParams(window.location.search); - const referralCode = urlParams.get('referralCode'); + const referralCode = getConfiguredReferralCode(); if (!referralCode) return; @@ -34,13 +67,8 @@ export function useRegisterReferralCode({ if (!result) return; - // Clean up URL parameter - urlParams.delete('referralCode'); - window.history.replaceState( - {}, - '', - `${window.location.pathname}${urlParams.toString() ? `?${urlParams.toString()}` : ''}` - ); + // Clean up URL parameters while leaving env-provided referral codes intact. + removeReferralParamsFromUrl(); if (result.success) { onSuccess?.();