'use client'; /** * Referral landing greeting: the `?ref=` half of the loyalty program. * * A referral link is `https://your-store.com/?ref=`, so this mounts on * the home page (see `src/app/page.tsx`), which is where those links land. * * ⛔ `getReferralInfo()` IS THE ONE LOYALTY CALL THAT NEEDS NO CUSTOMER TOKEN. * That is the entire reason this component can exist: it greets a visitor who * has no account yet, which is precisely the audience a referral link is aimed * at. Every other loyalty call requires a logged-in customer and would throw * here. * * ⛔ IT AUTO-HIDES FOUR WAYS, AND RENDERS NOTHING IN ALL OF THEM: no `?ref=` in * the URL, a malformed code, the merchant has referrals switched off * (`hasReferralProgram`), or the API says the code is not valid. A store with * no loyalty program never sees a trace of this. Build it anyway: the merchant * turns referrals on from the dashboard without touching this code. * * ⛔ CAPTURING MATTERS MORE THAN GREETING. The banner is the visible half, but * the useful half is `writeReferralCookie()`: almost nobody registers on the * page they land on, so the code is persisted the moment it arrives and read * back by the register form whenever the shopper gets round to signing up. Drop * the banner in a redesign if you like; do not drop the capture, or referrers * stop being credited and the failure is completely invisible. * * ⛔ Must be rendered inside a `` boundary. It uses * `useSearchParams()`, which opts the whole route out of static rendering * otherwise. */ import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import type { ReferralInfo } from 'brainerce'; import { Link } from '@/core/lib/navigation'; import { getClient } from '@/core/lib/brainerce'; import { useStoreCapabilities } from '@/core/providers/store-provider'; import { isGreetableReferral, normalizeReferralCode, writeReferralCookie, } from '@/core/lib/referral'; import { useTranslations } from '@/core/lib/translations'; import { cn } from '@/core/lib/utils'; interface ReferralGreetingProps { className?: string; } export function ReferralGreeting({ className }: ReferralGreetingProps) { const t = useTranslations('loyalty'); const searchParams = useSearchParams(); const { capabilities } = useStoreCapabilities(); const [info, setInfo] = useState(null); const code = normalizeReferralCode(searchParams.get('ref')); /** * Capabilities are fetched once at boot and are null until they land, or * forever if that fetch failed. Only an explicit `false` suppresses the * lookup, mirroring `canOfferStockAlert()`: an undecided flag must not * swallow a real referral, and `getReferralInfo()` independently answers * `{ valid: false }` when the program is off, so the feature stays correct * either way. */ const referralsOff = capabilities?.features.hasReferralProgram === false; useEffect(() => { if (!code || referralsOff) return; let cancelled = false; getClient() .getReferralInfo(code) .then((result) => { if (cancelled || !result.valid) return; // Persist BEFORE rendering anything. The visitor may navigate away in // the same second, and the code is the part that has to survive. writeReferralCookie(code); setInfo(result); }) .catch(() => { // Swallowed on purpose. An unreachable API on a landing page must not // break the home page, and there is nothing useful to say to a shopper // who does not yet know a referral was involved. }); return () => { cancelled = true; }; }, [code, referralsOff]); if (!isGreetableReferral(info)) return null; const greeting = info.referrerFirstName ? t('referralGreeting', { name: info.referrerFirstName }) : t('referralGreetingAnonymous'); return ( ); }