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:
@@ -174,8 +174,6 @@ export function SubscriptionPage() {
|
||||
|
||||
// ─── Search-param-driven flags (read once) ─────────────────────────────────
|
||||
const [welcome] = useState(() => search.welcome === "1");
|
||||
const [initialRef] = useState(() => search.ref ?? null);
|
||||
const hasAppliedRefRef = useRef(false);
|
||||
const stripeResultRef = useRef(false);
|
||||
|
||||
// ─── Stepper state ─────────────────────────────────────────────────────────
|
||||
@@ -299,12 +297,6 @@ export function SubscriptionPage() {
|
||||
}
|
||||
}, [subscription, currentPlanKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialRef || !subData || hasAppliedRefRef.current) return;
|
||||
hasAppliedRefRef.current = true;
|
||||
api.post("/referrals/apply", { code: initialRef.toUpperCase().trim() }).catch(() => {});
|
||||
}, [initialRef, subData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!welcome || !subData || hasFiredRef.current) return;
|
||||
if (!eligibleForTrial) {
|
||||
|
||||
Reference in New Issue
Block a user