diff --git a/apps/manacore/apps/web/src/lib/api/referrals.ts b/apps/manacore/apps/web/src/lib/api/referrals.ts
new file mode 100644
index 000000000..4b3b6ed59
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/api/referrals.ts
@@ -0,0 +1,149 @@
+/**
+ * Referrals Service for ManaCore Web App
+ * Handles referral codes, stats, and referral tracking
+ */
+
+import { authStore } from '$lib/stores/auth.svelte';
+
+const MANA_AUTH_URL = 'http://localhost:3001'; // TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env
+
+// Types
+export interface ReferralStats {
+ totalReferrals: number;
+ activeReferrals: number;
+ pendingReferrals: number;
+ qualifiedReferrals: number;
+ totalCreditsEarned: number;
+ currentTier: 'bronze' | 'silver' | 'gold' | 'platinum';
+ tierProgress: {
+ current: number;
+ nextTierAt: number;
+ percentToNext: number;
+ };
+}
+
+export interface ReferralCode {
+ code: string;
+ createdAt: string;
+ usageCount: number;
+ maxUses?: number;
+ expiresAt?: string;
+ bonusCredits: number;
+ referrerBonus: number;
+}
+
+export interface Referral {
+ id: string;
+ referredUserId: string;
+ referredEmail?: string;
+ stage: 'registered' | 'activated' | 'qualified' | 'retained';
+ creditsEarned: number;
+ registeredAt: string;
+ activatedAt?: string;
+ qualifiedAt?: string;
+ retainedAt?: string;
+}
+
+export interface ReferralValidation {
+ valid: boolean;
+ referrerName?: string;
+ bonusCredits?: number;
+ error?: string;
+}
+
+// Helper function for authenticated requests
+async function fetchWithAuth(endpoint: string, options: RequestInit = {}): Promise {
+ const token = await authStore.getAccessToken();
+
+ const response = await fetch(`${MANA_AUTH_URL}${endpoint}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ ...options.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ message: 'Request failed' }));
+ throw new Error(error.message || `HTTP ${response.status}`);
+ }
+
+ return response.json();
+}
+
+// Referrals Service
+export const referralsService = {
+ /**
+ * Get referral stats for the current user
+ */
+ async getStats(): Promise {
+ return fetchWithAuth('/api/v1/referrals/stats');
+ },
+
+ /**
+ * Get the user's referral code (creates one if doesn't exist)
+ */
+ async getCode(): Promise {
+ return fetchWithAuth('/api/v1/referrals/code');
+ },
+
+ /**
+ * Generate a new referral code
+ */
+ async generateCode(): Promise {
+ return fetchWithAuth('/api/v1/referrals/code', {
+ method: 'POST',
+ });
+ },
+
+ /**
+ * Get list of referrals made by the current user
+ */
+ async getReferrals(limit = 50, offset = 0): Promise {
+ return fetchWithAuth(`/api/v1/referrals/list?limit=${limit}&offset=${offset}`);
+ },
+
+ /**
+ * Validate a referral code (public endpoint - for registration)
+ */
+ async validateCode(code: string): Promise {
+ try {
+ const response = await fetch(`${MANA_AUTH_URL}/api/v1/referrals/validate/${code}`);
+ if (!response.ok) {
+ return { valid: false, error: 'Invalid code' };
+ }
+ const data = await response.json();
+ return {
+ valid: data.valid,
+ referrerName: data.referrerName,
+ bonusCredits: data.bonusCredits || 25,
+ error: data.error,
+ };
+ } catch {
+ return { valid: false, error: 'Validation failed' };
+ }
+ },
+
+ /**
+ * Get shareable referral link
+ */
+ getShareLink(code: string): string {
+ // Use current origin or fallback
+ const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'https://manacore.app';
+ return `${baseUrl}/register?ref=${code}`;
+ },
+
+ /**
+ * Copy referral link to clipboard
+ */
+ async copyShareLink(code: string): Promise {
+ try {
+ const link = this.getShareLink(code);
+ await navigator.clipboard.writeText(link);
+ return true;
+ } catch {
+ return false;
+ }
+ },
+};
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte
index ce932ed91..ed01f03cb 100644
--- a/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte
+++ b/apps/manacore/apps/web/src/lib/components/dashboard/WidgetContainer.svelte
@@ -16,6 +16,7 @@
import CreditsWidget from './widgets/CreditsWidget.svelte';
import QuickActionsWidget from './widgets/QuickActionsWidget.svelte';
import TransactionsWidget from './widgets/TransactionsWidget.svelte';
+ import ReferralWidget from './widgets/ReferralWidget.svelte';
import TasksTodayWidget from './widgets/TasksTodayWidget.svelte';
import TasksUpcomingWidget from './widgets/TasksUpcomingWidget.svelte';
import CalendarEventsWidget from './widgets/CalendarEventsWidget.svelte';
@@ -56,6 +57,7 @@
credits: CreditsWidget,
'quick-actions': QuickActionsWidget,
transactions: TransactionsWidget,
+ referral: ReferralWidget,
'tasks-today': TasksTodayWidget,
'tasks-upcoming': TasksUpcomingWidget,
'calendar-events': CalendarEventsWidget,
diff --git a/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ReferralWidget.svelte b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ReferralWidget.svelte
new file mode 100644
index 000000000..ab0174e3a
--- /dev/null
+++ b/apps/manacore/apps/web/src/lib/components/dashboard/widgets/ReferralWidget.svelte
@@ -0,0 +1,145 @@
+
+
+
+
+ 🤝
+ {$_('dashboard.widgets.referral.title')}
+
+
+ {#if state === 'loading'}
+
+ {:else if state === 'error'}
+
+ {:else if stats && code}
+
+
+
+
+ {$_('dashboard.widgets.referral.your_code')}
+
+
+ {code.code}
+
+
+
+
+
+
+
+
{stats.totalReferrals}
+
{$_('dashboard.widgets.referral.total')}
+
+
+
+{stats.totalCreditsEarned}
+
{$_('dashboard.widgets.referral.earned')}
+
+
+
+
+
+ {$_('dashboard.widgets.referral.tier')}
+
+ {getTierEmoji(stats.currentTier)}
+ {stats.currentTier.charAt(0).toUpperCase() + stats.currentTier.slice(1)}
+
+
+
+
+ {#if stats.tierProgress.nextTierAt > 0}
+
+
+ {stats.tierProgress.current} / {stats.tierProgress.nextTierAt}
+ {stats.tierProgress.percentToNext}%
+
+
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts
index 126070f7b..617b41d21 100644
--- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts
+++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts
@@ -121,15 +121,18 @@ export const authStore = {
/**
* Sign up with email and password
+ * @param email User email
+ * @param password User password
+ * @param referralCode Optional referral code for bonus credits
*/
- async signUp(email: string, password: string) {
+ async signUp(email: string, password: string, referralCode?: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
- const result = await authService.signUp(email, password);
+ const result = await authService.signUp(email, password, referralCode);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
@@ -149,6 +152,27 @@ export const authStore = {
}
},
+ /**
+ * Validate a referral code
+ */
+ async validateReferralCode(code: string) {
+ try {
+ const response = await fetch(`${getAuthUrl()}/api/v1/referrals/validate/${code}`);
+ if (!response.ok) {
+ return { valid: false, error: 'Invalid code' };
+ }
+ const data = await response.json();
+ return {
+ valid: data.valid,
+ referrerName: data.referrerName,
+ bonusCredits: data.bonusCredits || 25,
+ error: data.error,
+ };
+ } catch {
+ return { valid: false, error: 'Validation failed' };
+ }
+ },
+
/**
* Sign out
*/
diff --git a/apps/manacore/apps/web/src/lib/types/dashboard.ts b/apps/manacore/apps/web/src/lib/types/dashboard.ts
index 364a5caaf..c3bd6113b 100644
--- a/apps/manacore/apps/web/src/lib/types/dashboard.ts
+++ b/apps/manacore/apps/web/src/lib/types/dashboard.ts
@@ -11,6 +11,7 @@ export type WidgetType =
| 'credits' // Credits balance from mana-core-auth
| 'quick-actions' // Quick action links
| 'transactions' // Recent credit transactions
+ | 'referral' // Referral code and stats
| 'tasks-today' // Todo API: today's tasks
| 'tasks-upcoming' // Todo API: upcoming 7 days
| 'calendar-events' // Calendar API: upcoming events
@@ -146,6 +147,15 @@ export const WIDGET_REGISTRY: WidgetMeta[] = [
allowMultiple: false,
requiredBackend: 'mana-core-auth',
},
+ {
+ type: 'referral',
+ nameKey: 'dashboard.widgets.referral.title',
+ descriptionKey: 'dashboard.widgets.referral.description',
+ icon: '🤝',
+ defaultSize: 'medium',
+ allowMultiple: false,
+ requiredBackend: 'mana-core-auth',
+ },
{
type: 'tasks-today',
nameKey: 'dashboard.widgets.tasks_today.title',
diff --git a/apps/manacore/apps/web/src/routes/(auth)/register/+page.svelte b/apps/manacore/apps/web/src/routes/(auth)/register/+page.svelte
index 9ebf19621..71979b19e 100644
--- a/apps/manacore/apps/web/src/routes/(auth)/register/+page.svelte
+++ b/apps/manacore/apps/web/src/routes/(auth)/register/+page.svelte
@@ -1,12 +1,20 @@
@@ -15,6 +23,9 @@
logo={ManaCoreLogo}
primaryColor="#6366f1"
onSignUp={handleSignUp}
+ onValidateReferralCode={handleValidateReferralCode}
+ {initialReferralCode}
+ baseSignupCredits={25}
{goto}
successRedirect="/dashboard"
loginPath="/login"
diff --git a/packages/shared-auth-ui/src/pages/RegisterPage.svelte b/packages/shared-auth-ui/src/pages/RegisterPage.svelte
index 4b80f787e..688337dd8 100644
--- a/packages/shared-auth-ui/src/pages/RegisterPage.svelte
+++ b/packages/shared-auth-ui/src/pages/RegisterPage.svelte
@@ -17,6 +17,12 @@
backToLogin: string;
showPassword: string;
hidePassword: string;
+ // Referral
+ referralCodePlaceholder: string;
+ referralCodeValid: string;
+ referralCodeInvalid: string;
+ referralCodeValidating: string;
+ referralBonusCredits: string;
// Error messages
emailRequired: string;
passwordRequired: string;
@@ -29,6 +35,14 @@
accountCreated: string;
}
+ /** Referral code validation result */
+ interface ReferralValidation {
+ valid: boolean;
+ referrerName?: string;
+ bonusCredits?: number;
+ error?: string;
+ }
+
/** Default English translations */
const defaultTranslations: RegisterTranslations = {
title: 'Create Account',
@@ -42,6 +56,13 @@
backToLogin: 'Back to Login',
showPassword: 'Show password',
hidePassword: 'Hide password',
+ // Referral
+ referralCodePlaceholder: 'Referral Code (optional)',
+ referralCodeValid: 'Valid code!',
+ referralCodeInvalid: 'Invalid code',
+ referralCodeValidating: 'Checking...',
+ referralBonusCredits: 'bonus credits',
+ // Error messages
emailRequired: 'Email is required',
passwordRequired: 'Password is required',
confirmPasswordRequired: 'Please confirm your password',
@@ -60,8 +81,8 @@
logo: Component<{ size?: number; color?: string }>;
/** Primary color (hex) */
primaryColor: string;
- /** Sign up function */
- onSignUp: (email: string, password: string) => Promise;
+ /** Sign up function (with optional referral code) */
+ onSignUp: (email: string, password: string, referralCode?: string) => Promise;
/** Navigation function */
goto: (path: string) => void;
/** Success redirect path */
@@ -78,6 +99,12 @@
headerControls?: Snippet;
/** Translations for i18n support */
translations?: Partial;
+ /** Referral code validation function */
+ onValidateReferralCode?: (code: string) => Promise;
+ /** Initial referral code (e.g., from URL) */
+ initialReferralCode?: string;
+ /** Base signup credits (shown to user) */
+ baseSignupCredits?: number;
}
let {
@@ -93,6 +120,9 @@
appSlider,
headerControls,
translations = {},
+ onValidateReferralCode,
+ initialReferralCode = '',
+ baseSignupCredits = 25,
}: Props = $props();
// Merge provided translations with defaults
@@ -108,6 +138,17 @@
let showPassword = $state(false);
let showConfirmPassword = $state(false);
+ // Referral state
+ let referralCode = $state(initialReferralCode);
+ let referralValidation = $state(null);
+ let validatingReferral = $state(false);
+ let referralDebounceTimer: ReturnType | null = null;
+
+ // Calculate total credits (base + bonus)
+ let totalCredits = $derived(
+ baseSignupCredits + (referralValidation?.valid ? referralValidation.bonusCredits || 0 : 0)
+ );
+
// Theme state - can be toggled manually, defaults to system preference
let userThemePreference = $state<'light' | 'dark' | null>(null);
let systemIsDark = $state(
@@ -139,6 +180,44 @@
}
}
+ // Referral code validation
+ async function validateReferralCode(code: string) {
+ if (!code || code.length < 3 || !onValidateReferralCode) {
+ referralValidation = null;
+ return;
+ }
+
+ validatingReferral = true;
+ try {
+ const result = await onValidateReferralCode(code);
+ referralValidation = result;
+ } catch {
+ referralValidation = { valid: false, error: 'Validation failed' };
+ } finally {
+ validatingReferral = false;
+ }
+ }
+
+ function handleReferralCodeChange(value: string) {
+ referralCode = value.toUpperCase().replace(/[^A-Z0-9]/g, '');
+
+ // Clear previous timer
+ if (referralDebounceTimer) {
+ clearTimeout(referralDebounceTimer);
+ }
+
+ // Reset validation if empty
+ if (!referralCode) {
+ referralValidation = null;
+ return;
+ }
+
+ // Debounce validation
+ referralDebounceTimer = setTimeout(() => {
+ validateReferralCode(referralCode);
+ }, 500);
+ }
+
// Password validation
let passwordRequirements = $derived.by(() => {
if (!password) {
@@ -212,7 +291,9 @@
return;
}
- const result = await onSignUp(email, password);
+ // Pass referral code if valid
+ const validReferralCode = referralValidation?.valid ? referralCode : undefined;
+ const result = await onSignUp(email, password, validReferralCode);
loading = false;
@@ -415,6 +496,58 @@
{t.passwordRequirements}
+
+ {#if onValidateReferralCode}
+
+
+
handleReferralCodeChange((e.target as HTMLInputElement).value)}
+ placeholder={t.referralCodePlaceholder}
+ maxlength={12}
+ class="h-14 w-full rounded-xl border px-4 pr-24 text-lg transition-colors focus:outline-none focus:ring-2 uppercase tracking-wider"
+ style="background-color: {isDark
+ ? 'rgba(0, 0, 0, 0.2)'
+ : 'rgba(255, 255, 255, 0.8)'}; border-color: {referralValidation?.valid
+ ? '#22c55e'
+ : referralValidation && !referralValidation.valid
+ ? '#ef4444'
+ : isDark
+ ? 'rgba(255, 255, 255, 0.2)'
+ : 'rgba(0, 0, 0, 0.1)'}; color: {isDark
+ ? '#ffffff'
+ : '#000000'}; --tw-ring-color: {primaryColor};"
+ />
+
+
+ {#if validatingReferral}
+
+ {t.referralCodeValidating}
+
+ {:else if referralValidation?.valid}
+ ✓ {t.referralCodeValid}
+ {:else if referralValidation && !referralValidation.valid}
+ ✗ {t.referralCodeInvalid}
+ {/if}
+
+
+
+ {#if referralValidation?.valid && referralValidation.bonusCredits}
+
+ +{referralValidation.bonusCredits}
+ {t.referralBonusCredits}
+ {#if referralValidation.referrerName}
+ (von {referralValidation.referrerName})
+ {/if}
+
+ {/if}
+
+ {/if}
+