Skip to content

Commit 5b8f798

Browse files
committed
Add SaveReferral component and unit tests: ensure app-wide referral capture and fix attribution issues
1 parent 31d8299 commit 5b8f798

4 files changed

Lines changed: 62 additions & 1 deletion

File tree

web/components/save-referral.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {useSaveReferral} from 'web/hooks/use-save-referral'
2+
import {useUser} from 'web/hooks/use-user'
3+
4+
/**
5+
* Records the `?referrer=` (or base64 `?r=`) query param on whatever page the visitor lands on.
6+
*
7+
* Mounted app-wide on purpose: referral links point at the home page (`/?referrer=alice`), the
8+
* about page, an event — not just profiles. `useSaveReferral` used to run only on `/[username]`,
9+
* so every share that wasn't a profile link lost its attribution.
10+
*/
11+
export function SaveReferral() {
12+
const user = useUser()
13+
useSaveReferral(user)
14+
return null
15+
}

web/hooks/use-save-referral.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ export const useSaveReferral = (
1313
const {searchParams} = useDefinedSearchParams()
1414

1515
useEffect(() => {
16+
// Null on the first render of a statically-optimized page — the query is only readable once the
17+
// router is ready, which re-runs this effect. Don't blow up in between.
18+
if (!searchParams) return
19+
1620
const referrer = searchParams.get('r')
1721
? decodeBase64(searchParams.get('r') as string)
18-
: (searchParams.get('referrer') as string)
22+
: (searchParams.get('referrer') ?? undefined)
1923

2024
const referrerOrDefault = referrer || options?.defaultReferrerUsername
2125

web/pages/_app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {LiveRegionProvider} from 'web/components/live-region'
2727
import {UnseenMessageChannelsProvider} from 'web/components/messaging/messages-icon'
2828
import {PrivateMessageMembershipsProvider} from 'web/components/messaging/private-message-memberships-context'
2929
import {ReviewPrompts} from 'web/components/review-prompts'
30+
import {SaveReferral} from 'web/components/save-referral'
3031
import {ChoicesProvider} from 'web/hooks/use-choices'
3132
import {useFontPreferenceManager} from 'web/hooks/use-font-preference'
3233
import {useHasLoaded} from 'web/hooks/use-has-loaded'
@@ -342,6 +343,7 @@ function MyApp(props: AppProps<PageProps>) {
342343
<WebPush />
343344
<NativePush />
344345
<ReviewPrompts />
346+
<SaveReferral />
345347
<Component {...pageProps} />
346348
</PrivateMessageMembershipsProvider>
347349
</UnseenMessageChannelsProvider>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {readFileSync} from 'fs'
2+
import {join} from 'path'
3+
4+
/**
5+
* `?referrer=` attribution is invisible when it breaks: the visitor signs up fine, the referrer just
6+
* never gets credited, and nothing in the UI says so. It broke exactly once already — `useSaveReferral`
7+
* was mounted only on `/[username]`, so `compassmeet.com/Martin` saved the referral but
8+
* `compassmeet.com/?referrer=Martin` (the link the referrals page hands out) silently dropped it.
9+
*
10+
* `testEnvironment` is `node` and there's no DOM test renderer here, so the effect itself can't be
11+
* exercised. These assertions pin the two things whose absence caused that bug instead: the hook is
12+
* mounted app-wide, and the component doing the mounting actually calls it.
13+
*/
14+
15+
const read = (...parts: string[]) => readFileSync(join(__dirname, '..', '..', ...parts), 'utf8')
16+
17+
describe('referral capture', () => {
18+
it('is mounted app-wide, not only on the profile page', () => {
19+
const app = read('pages', '_app.tsx')
20+
21+
expect(app).toContain("from 'web/components/save-referral'")
22+
expect(app).toContain('<SaveReferral />')
23+
})
24+
25+
it('SaveReferral feeds the signed-in user to useSaveReferral', () => {
26+
const component = read('components', 'save-referral.tsx')
27+
28+
// The hook only writes for `user === null` (logged out), so passing the user is what makes it
29+
// wait for auth to resolve instead of writing on every page for everyone.
30+
expect(component).toContain('useSaveReferral(user)')
31+
expect(component).toContain('useUser()')
32+
})
33+
34+
it('reads both the plain and the base64 referrer params', () => {
35+
const hook = read('hooks', 'use-save-referral.ts')
36+
37+
expect(hook).toContain("searchParams.get('referrer')")
38+
expect(hook).toContain("searchParams.get('r')")
39+
})
40+
})

0 commit comments

Comments
 (0)