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
72 changes: 71 additions & 1 deletion packages/sdk/echo-start/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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'];

Expand Down
50 changes: 39 additions & 11 deletions packages/sdk/react/src/hooks/useRegisterReferralCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined> } };
process?: { env?: Record<string, string | undefined> };
};

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,
Expand All @@ -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;

Expand All @@ -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?.();
Expand Down