feat(referrals): rework reward engine + email-verified landing
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Reward engine: - Recurring milestones (every 3 → +7d, every 5 → +14d) instead of one-time tiers capped at 5; idempotent + transactional grants serialised per referrer so concurrent qualifications can't double-count. - Rewards now gated on the referred user's email verification (afterEmailVerification hook); already-verified referees (OAuth) qualify at apply time. - Reward days banked as users.referral_credit_days when the referrer has no live subscription, consumed on next trial start / activation (no more silently lost rewards). - Accurate cumulative rewardDays in stats; getMyReferrals returns referee name/masked email/status. Hardening / cleanup: - onConflictDoNothing makes apply idempotent (no unhandled unique violation). - Anti-fraud: normalizeEmail blocks self-referral via gmail dot/+tag aliases. - Collision-safe referral code generation at signup. - Single apply path (welcome onboarding modal); removed duplicate calls in register + subscription pages. Input validation on the apply code. Email verification UX: - Verification link now lands on a dedicated /email-verified confirmation page instead of the deep-linked VIN/search page. Schema: referrals.status + qualified_at, users.referral_credit_days (0009). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import { Route as AuthResetPasswordRouteImport } from './routes/_auth/reset-pass
|
||||
import { Route as AuthRegisterRouteImport } from './routes/_auth/register'
|
||||
import { Route as AuthLoginRouteImport } from './routes/_auth/login'
|
||||
import { Route as AuthForgotPasswordRouteImport } from './routes/_auth/forgot-password'
|
||||
import { Route as AuthEmailVerifiedRouteImport } from './routes/_auth/email-verified'
|
||||
import { Route as DashboardSubscriptionIndexRouteImport } from './routes/dashboard/subscription/index'
|
||||
import { Route as DashboardCatalogIndexRouteImport } from './routes/dashboard/catalog/index'
|
||||
import { Route as DashboardAdminIndexRouteImport } from './routes/dashboard/admin/index'
|
||||
@@ -160,6 +161,11 @@ const AuthForgotPasswordRoute = AuthForgotPasswordRouteImport.update({
|
||||
path: '/forgot-password',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthEmailVerifiedRoute = AuthEmailVerifiedRouteImport.update({
|
||||
id: '/email-verified',
|
||||
path: '/email-verified',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const DashboardSubscriptionIndexRoute =
|
||||
DashboardSubscriptionIndexRouteImport.update({
|
||||
id: '/subscription/',
|
||||
@@ -280,6 +286,7 @@ export interface FileRoutesByFullPath {
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/email-verified': typeof AuthEmailVerifiedRoute
|
||||
'/forgot-password': typeof AuthForgotPasswordRoute
|
||||
'/login': typeof AuthLoginRoute
|
||||
'/register': typeof AuthRegisterRoute
|
||||
@@ -321,6 +328,7 @@ export interface FileRoutesByTo {
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/email-verified': typeof AuthEmailVerifiedRoute
|
||||
'/forgot-password': typeof AuthForgotPasswordRoute
|
||||
'/login': typeof AuthLoginRoute
|
||||
'/register': typeof AuthRegisterRoute
|
||||
@@ -365,6 +373,7 @@ export interface FileRoutesById {
|
||||
'/pricing': typeof PricingRoute
|
||||
'/privacy': typeof PrivacyRoute
|
||||
'/terms': typeof TermsRoute
|
||||
'/_auth/email-verified': typeof AuthEmailVerifiedRoute
|
||||
'/_auth/forgot-password': typeof AuthForgotPasswordRoute
|
||||
'/_auth/login': typeof AuthLoginRoute
|
||||
'/_auth/register': typeof AuthRegisterRoute
|
||||
@@ -409,6 +418,7 @@ export interface FileRouteTypes {
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/email-verified'
|
||||
| '/forgot-password'
|
||||
| '/login'
|
||||
| '/register'
|
||||
@@ -450,6 +460,7 @@ export interface FileRouteTypes {
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/email-verified'
|
||||
| '/forgot-password'
|
||||
| '/login'
|
||||
| '/register'
|
||||
@@ -493,6 +504,7 @@ export interface FileRouteTypes {
|
||||
| '/pricing'
|
||||
| '/privacy'
|
||||
| '/terms'
|
||||
| '/_auth/email-verified'
|
||||
| '/_auth/forgot-password'
|
||||
| '/_auth/login'
|
||||
| '/_auth/register'
|
||||
@@ -696,6 +708,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthForgotPasswordRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/email-verified': {
|
||||
id: '/_auth/email-verified'
|
||||
path: '/email-verified'
|
||||
fullPath: '/email-verified'
|
||||
preLoaderRoute: typeof AuthEmailVerifiedRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/dashboard/subscription/': {
|
||||
id: '/dashboard/subscription/'
|
||||
path: '/subscription'
|
||||
@@ -833,6 +852,7 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
|
||||
interface AuthRouteChildren {
|
||||
AuthEmailVerifiedRoute: typeof AuthEmailVerifiedRoute
|
||||
AuthForgotPasswordRoute: typeof AuthForgotPasswordRoute
|
||||
AuthLoginRoute: typeof AuthLoginRoute
|
||||
AuthRegisterRoute: typeof AuthRegisterRoute
|
||||
@@ -840,6 +860,7 @@ interface AuthRouteChildren {
|
||||
}
|
||||
|
||||
const AuthRouteChildren: AuthRouteChildren = {
|
||||
AuthEmailVerifiedRoute: AuthEmailVerifiedRoute,
|
||||
AuthForgotPasswordRoute: AuthForgotPasswordRoute,
|
||||
AuthLoginRoute: AuthLoginRoute,
|
||||
AuthRegisterRoute: AuthRegisterRoute,
|
||||
|
||||
Reference in New Issue
Block a user