Skip to content

Commit 526c129

Browse files
committed
feat(referrals): add Kilo Pass sharing entry points
1 parent c24d695 commit 526c129

12 files changed

Lines changed: 281 additions & 277 deletions

File tree

.specs/impact-referrals.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,9 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
153153
- UTT UUID: `A7138521-9724-4b8f-95f4-1db2fbae81141`
154154
- Advocate widget ID: `p/51699/w/referrerWidget`
155155

156-
3. Existing unscoped Impact Advocate configuration MAY remain as KiloClaw fallback configuration only. Kilo Pass MUST
157-
require explicit Kilo Pass Advocate program/widget configuration and MUST NOT fall back to KiloClaw configuration.
156+
3. Impact Advocate account SID, auth token, and tenant alias MAY be shared across Advocate programs. KiloClaw and Kilo
157+
Pass MUST each require explicit product-scoped Advocate program ID and widget ID configuration. Products MUST NOT
158+
fall back to unscoped or other-product program/widget configuration.
158159

159160
4. Kilo Pass MUST use a different Impact Advocate program ID and widget ID than KiloClaw.
160161

@@ -715,6 +716,11 @@ conversion, local referral rewards are authoritative and affiliate SALE reportin
715716

716717
## Changelog
717718

719+
### 2026-05-25 -- Require product-scoped Advocate program/widget configuration
720+
721+
Removed KiloClaw fallback to unscoped Impact Advocate program/widget configuration. KiloClaw and Kilo Pass now both
722+
require explicit product-scoped Advocate program ID and widget ID while sharing account SID, auth token, and tenant alias.
723+
718724
### 2026-05-22 -- Rename and expand to Kilo Pass
719725

720726
Renamed `.specs/kiloclaw-referrals.md` to `.specs/impact-referrals.md`. Generalized shared Impact Advocate referral

apps/web/src/components/profile/kilo-pass/KiloPassActiveSubscriptionCard.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useState } from 'react';
55
import { useQuery } from '@tanstack/react-query';
66

77
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
8-
import { Badge } from '@/components/ui/badge';
8+
import { SubscriptionStatusBadge } from '@/components/subscriptions/SubscriptionStatusBadge';
99
import { Button } from '@/components/ui/button';
1010
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1111
import { formatDollars, formatIsoDateString_UsaDateOnlyFormat } from '@/lib/utils';
@@ -14,6 +14,8 @@ import { dayjs } from '@/lib/kilo-pass/dayjs';
1414
import { useTRPC } from '@/lib/trpc/utils';
1515
import { getMonthlyPriceUsd } from '@/lib/kilo-pass/bonus';
1616

17+
import { KiloPassReferralButton } from '@/components/referrals/KiloPassReferralButton';
18+
1719
import { KiloPassSubscriptionSettingsModal } from './KiloPassSubscriptionSettingsModal';
1820
import type { KiloPassSubscription } from './kiloPassSubscription';
1921
import {
@@ -140,15 +142,18 @@ function HeaderRow() {
140142
<Coins className="h-5 w-5 text-amber-300" />
141143
</span>
142144
<span className="leading-none">
143-
<span className="block text-base">Kilo Pass</span>
145+
<span className="flex flex-wrap items-center gap-2">
146+
<span className="block text-base">Kilo Pass</span>
147+
<SubscriptionStatusBadge status={view.status.kind} />
148+
</span>
144149
<span className="text-muted-foreground block text-sm font-normal">
145150
{view.header.tierLabel}{view.header.cadenceLabel}
146151
</span>
147152
</span>
148153
</CardTitle>
149154

150-
<div className="flex items-center gap-2">
151-
<Badge variant={view.status.badgeVariant}>{view.status.label}</Badge>
155+
<div className="flex flex-wrap items-center justify-end gap-2">
156+
<KiloPassReferralButton />
152157
{providerManagement.externalManagementAction ? (
153158
<Button asChild variant="outline" size="icon" className="h-9 w-9">
154159
<a
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Link from 'next/link';
2+
import { Gift } from 'lucide-react';
3+
4+
import { Button } from '@/components/ui/button';
5+
import { cn } from '@/lib/utils';
6+
7+
export function KiloPassReferralButton({ className }: { className?: string }) {
8+
return (
9+
<Button asChild variant="outline" className={cn('h-9 gap-2 pr-2.5', className)}>
10+
<Link href="/subscriptions/kilo-pass/refer">
11+
<Gift className="h-4 w-4" aria-hidden="true" />
12+
<span>Refer &amp; earn</span>
13+
<span className="bg-brand-primary text-primary-foreground rounded-full px-1.5 py-0.5 text-[10px] leading-none font-bold tracking-wide uppercase ring-1 ring-brand-primary/30">
14+
New
15+
</span>
16+
</Link>
17+
</Button>
18+
);
19+
}

apps/web/src/components/referrals/KiloPassReferralPageContent.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ describe('KiloPassReferralPageContent', () => {
3434
expect(html).toContain('Earn Kilo Pass referral bonuses');
3535
expect(html).toContain('50% monthly Kilo Pass bonus');
3636
expect(html).toContain('No Kilo Pass referral rewards yet.');
37-
expect(html).toContain('aria-labelledby="kilo-pass-referral-widget-heading"');
37+
expect(html).toContain('aria-label="Kilo Pass referral sharing"');
3838
expect(html).toContain('data-testid="share-widget"');
39+
expect(html).not.toContain('Share your Kilo Pass referral link');
40+
expect(html).not.toContain('Use the Kilo Pass referral widget');
3941
expect(html).not.toContain('KiloClaw');
4042
expect(html).not.toContain('free month');
4143
});

apps/web/src/components/referrals/KiloPassReferralPageContent.tsx

Lines changed: 92 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CalendarDays, Gift, History, Info, Sparkles } from 'lucide-react';
66

77
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
88
import { Button } from '@/components/ui/button';
9-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
9+
import { Card, CardContent } from '@/components/ui/card';
1010
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
1111
import { formatIsoDateString_UsaDateOnlyFormat } from '@/lib/utils';
1212

@@ -138,7 +138,7 @@ export function KiloPassReferralPageContent({
138138
<div className="space-y-2">
139139
<h1 className="text-3xl font-bold tracking-tight">Earn Kilo Pass referral bonuses</h1>
140140
<p className="max-w-3xl text-sm leading-6 text-muted-foreground">
141-
Share Kilo Pass with another developer. When their first eligible monthly payment is
141+
Share Kilo Pass with someone else and when their first eligible monthly payment is
142142
confirmed, you both earn a 50% monthly Kilo Pass bonus based on their tier.
143143
</p>
144144
</div>
@@ -149,30 +149,10 @@ export function KiloPassReferralPageContent({
149149
</div>
150150

151151
<Card>
152-
<CardHeader>
153-
<CardTitle>How rewards apply</CardTitle>
154-
<CardDescription>
155-
One pending referral reward is consumed per eligible monthly base issuance. Referral
156-
bonus credits replace the normal monthly bonus for that issuance, and unused bonus
157-
credits expire at month end.
158-
</CardDescription>
159-
</CardHeader>
160-
</Card>
161-
162-
<Card>
163-
<CardHeader>
164-
<CardTitle id="kilo-pass-referral-widget-heading" className="flex items-center gap-2">
165-
<Sparkles className="size-4" aria-hidden="true" />
166-
Share your Kilo Pass referral link
167-
</CardTitle>
168-
<CardDescription>
169-
Use the Kilo Pass referral widget to copy your link and track Impact Advocate sharing.
170-
</CardDescription>
171-
</CardHeader>
172-
<CardContent>
152+
<CardContent className="space-y-6 p-6">
173153
<section
174154
id={SHARE_WIDGET_ANCHOR_ID}
175-
aria-labelledby="kilo-pass-referral-widget-heading"
155+
aria-label="Kilo Pass referral sharing"
176156
className="rounded-lg border border-border bg-input/30 p-4"
177157
>
178158
{shareWidget ?? (
@@ -181,100 +161,107 @@ export function KiloPassReferralPageContent({
181161
</div>
182162
)}
183163
</section>
164+
165+
{isLoading ? (
166+
<div
167+
className="border-t border-border pt-6 text-sm text-muted-foreground"
168+
role="status"
169+
>
170+
Loading Kilo Pass referral rewards…
171+
</div>
172+
) : errorMessage ? (
173+
<div className="border-t border-border pt-6">
174+
<Alert variant="destructive" role="alert">
175+
<AlertTitle>Kilo Pass referral rewards are unavailable</AlertTitle>
176+
<AlertDescription>{errorMessage || 'Try again in a minute.'}</AlertDescription>
177+
</Alert>
178+
</div>
179+
) : summary ? (
180+
<KiloPassReferralSummary summary={summary} />
181+
) : null}
184182
</CardContent>
185183
</Card>
186-
187-
{isLoading ? (
188-
<Card>
189-
<CardContent className="p-6 text-sm text-muted-foreground" role="status">
190-
Loading Kilo Pass referral rewards…
191-
</CardContent>
192-
</Card>
193-
) : errorMessage ? (
194-
<Alert variant="destructive" role="alert">
195-
<AlertTitle>Kilo Pass referral rewards are unavailable</AlertTitle>
196-
<AlertDescription>{errorMessage || 'Try again in a minute.'}</AlertDescription>
197-
</Alert>
198-
) : summary ? (
199-
<KiloPassReferralSummary summary={summary} />
200-
) : null}
201184
</div>
202185
);
203186
}
204187

205188
function KiloPassReferralSummary({ summary }: { summary: KiloPassReferralRewardSummary }) {
206189
return (
207-
<Card>
208-
<CardHeader>
209-
<CardTitle>Reward summary</CardTitle>
210-
<CardDescription>
190+
<section
191+
aria-labelledby="kilo-pass-referral-summary-heading"
192+
className="space-y-6 border-t border-border pt-6"
193+
>
194+
<div className="space-y-1.5">
195+
<h2 id="kilo-pass-referral-summary-heading" className="font-semibold tracking-tight">
196+
Reward summary
197+
</h2>
198+
<p className="text-sm text-muted-foreground">
211199
Track pending referral bonuses and previous Kilo Pass referral reward history.
212-
</CardDescription>
213-
</CardHeader>
214-
<CardContent className="space-y-6">
215-
{summary.referrerCap.reached ? (
216-
<div className="flex flex-col gap-2 rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
217-
<div>
218-
<div className="font-medium text-yellow-400">Cap reached</div>
219-
<div className="text-muted-foreground">
220-
{summary.referrerCap.grantedRewards} of {summary.referrerCap.limit} referrer rewards
221-
granted. Referee rewards do not count toward this cap.
222-
</div>
200+
</p>
201+
</div>
202+
203+
{summary.referrerCap.reached ? (
204+
<div className="flex flex-col gap-2 rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-3 text-sm sm:flex-row sm:items-center sm:justify-between">
205+
<div>
206+
<div className="font-medium text-yellow-400">Cap reached</div>
207+
<div className="text-muted-foreground">
208+
{summary.referrerCap.grantedRewards} of {summary.referrerCap.limit} referrer rewards
209+
granted. Referee rewards do not count toward this cap.
223210
</div>
224211
</div>
225-
) : null}
226-
227-
<div className="grid gap-3 md:grid-cols-3">
228-
<SummaryTile label="Total rewards" value={String(summary.totals.totalRewards)} />
229-
<SummaryTile
230-
label="Pending rewards"
231-
value={String(summary.totals.pendingRewards)}
232-
info="Pending rewards wait for a future eligible monthly Kilo Pass issuance."
233-
indicator={summary.totals.pendingRewards > 0 ? 'warning' : undefined}
234-
/>
235-
<SummaryTile label="Applied rewards" value={String(summary.totals.appliedRewards)} />
236-
<SummaryTile
237-
label="Total bonus value"
238-
value={formatUsd(summary.totals.totalRewardAmountUsd)}
239-
/>
240-
<SummaryTile
241-
label="Pending bonus value"
242-
value={formatUsd(summary.totals.pendingRewardAmountUsd)}
243-
/>
244-
<SummaryTile
245-
label="Applied bonus value"
246-
value={formatUsd(summary.totals.appliedRewardAmountUsd)}
247-
/>
248212
</div>
213+
) : null}
249214

250-
<section aria-labelledby="kilo-pass-rewards-heading" className="space-y-3">
251-
<h2 id="kilo-pass-rewards-heading" className="text-sm font-semibold text-foreground">
252-
Reward history
253-
</h2>
254-
{summary.rewards.length === 0 ? (
255-
<div className="rounded-lg border border-dashed border-border p-4 text-sm text-muted-foreground">
256-
No Kilo Pass referral rewards yet.{' '}
257-
<a
258-
href={`#${SHARE_WIDGET_ANCHOR_ID}`}
259-
className="rounded-sm text-foreground underline decoration-foreground/35 underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
260-
>
261-
Share your referral link
262-
</a>{' '}
263-
to earn a future monthly bonus.
264-
</div>
265-
) : (
266-
<div className="divide-y divide-border rounded-lg border border-border">
267-
{summary.rewards.map((reward, index) => (
268-
<RewardRow
269-
key={`${reward.role}-${reward.status}-${reward.earnedAt}-${index}`}
270-
reward={reward}
271-
/>
272-
))}
273-
</div>
274-
)}
275-
</section>
276-
</CardContent>
277-
</Card>
215+
<div className="grid gap-3 md:grid-cols-3">
216+
<SummaryTile label="Total rewards" value={String(summary.totals.totalRewards)} />
217+
<SummaryTile
218+
label="Pending rewards"
219+
value={String(summary.totals.pendingRewards)}
220+
info="Pending rewards wait for a future eligible monthly Kilo Pass issuance."
221+
indicator={summary.totals.pendingRewards > 0 ? 'warning' : undefined}
222+
/>
223+
<SummaryTile label="Applied rewards" value={String(summary.totals.appliedRewards)} />
224+
<SummaryTile
225+
label="Total bonus value"
226+
value={formatUsd(summary.totals.totalRewardAmountUsd)}
227+
/>
228+
<SummaryTile
229+
label="Pending bonus value"
230+
value={formatUsd(summary.totals.pendingRewardAmountUsd)}
231+
/>
232+
<SummaryTile
233+
label="Applied bonus value"
234+
value={formatUsd(summary.totals.appliedRewardAmountUsd)}
235+
/>
236+
</div>
237+
238+
<section aria-labelledby="kilo-pass-rewards-heading" className="space-y-3">
239+
<h3 id="kilo-pass-rewards-heading" className="text-sm font-semibold text-foreground">
240+
Reward history
241+
</h3>
242+
{summary.rewards.length === 0 ? (
243+
<div className="rounded-lg border border-dashed border-border p-4 text-sm text-muted-foreground">
244+
No Kilo Pass referral rewards yet.{' '}
245+
<a
246+
href={`#${SHARE_WIDGET_ANCHOR_ID}`}
247+
className="rounded-sm text-foreground underline decoration-foreground/35 underline-offset-2 hover:decoration-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
248+
>
249+
Share your referral link
250+
</a>{' '}
251+
to earn a future monthly bonus.
252+
</div>
253+
) : (
254+
<div className="divide-y divide-border rounded-lg border border-border">
255+
{summary.rewards.map((reward, index) => (
256+
<RewardRow
257+
key={`${reward.role}-${reward.status}-${reward.earnedAt}-${index}`}
258+
reward={reward}
259+
/>
260+
))}
261+
</div>
262+
)}
263+
</section>
264+
</section>
278265
);
279266
}
280267

apps/web/src/components/subscriptions/PersonalSubscriptions.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import { KiloClawGroup } from './kiloclaw/KiloClawGroup';
1212
import { CodingPlansGroup } from './coding-plans/CodingPlansGroup';
1313
import { ENABLE_CODING_PLAN_SUBSCRIPTIONS } from '@/lib/constants';
1414

15+
const defaultExpandedSections = ENABLE_CODING_PLAN_SUBSCRIPTIONS
16+
? ['kilo-pass', 'kiloclaw', 'coding-plans']
17+
: ['kilo-pass', 'kiloclaw'];
18+
1519
export function PersonalSubscriptions() {
1620
const [showTerminal, setShowTerminal] = useState(false);
17-
const [expandedSection, setExpandedSection] = useState('kilo-pass');
1821
const trpc = useTRPC();
1922
const kiloPassQuery = useQuery(trpc.kiloPass.getState.queryOptions());
2023
const kiloClawQuery = useQuery(trpc.kiloclaw.listPersonalSubscriptions.queryOptions());
@@ -41,13 +44,7 @@ export function PersonalSubscriptions() {
4144
) : null
4245
}
4346
>
44-
<Accordion
45-
type="single"
46-
collapsible
47-
value={expandedSection}
48-
onValueChange={setExpandedSection}
49-
className="space-y-8"
50-
>
47+
<Accordion type="multiple" defaultValue={defaultExpandedSections} className="space-y-8">
5148
<KiloPassGroup showTerminal={showTerminal} accordionValue="kilo-pass" />
5249
<KiloClawGroup showTerminal={showTerminal} accordionValue="kiloclaw" />
5350
{ENABLE_CODING_PLAN_SUBSCRIPTIONS ? (

apps/web/src/components/subscriptions/kilo-pass/KiloPassDetail.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22

3-
import Link from 'next/link';
43
import { useCallback, useMemo, useState } from 'react';
54
import { useQuery, useQueryClient } from '@tanstack/react-query';
65
import { toast } from 'sonner';
@@ -22,6 +21,7 @@ import { cn } from '@/lib/utils';
2221
import { useRawTRPCClient, useTRPC } from '@/lib/trpc/utils';
2322
import { formatDollars, formatIsoDateString_UsaDateOnlyFormat } from '@/lib/utils';
2423
import { DetailPageHeader } from '@/components/subscriptions/DetailPageHeader';
24+
import { KiloPassReferralButton } from '@/components/referrals/KiloPassReferralButton';
2525
import { BillingHistoryTable } from '@/components/subscriptions/BillingHistoryTable';
2626
import { CreditHistory } from './CreditHistory';
2727
import {
@@ -190,6 +190,7 @@ export function KiloPassDetail() {
190190
backLabel="Back to subscriptions"
191191
title="Kilo Pass"
192192
status={subscriptionDisplay.status}
193+
actions={isKiloPassTerminal(subscription.status) ? null : <KiloPassReferralButton />}
193194
/>
194195

195196
{subscriptionDisplay.detailAlert ? (
@@ -384,9 +385,6 @@ function KiloPassInlineActions({
384385
return (
385386
<>
386387
<div className="flex flex-wrap gap-2">
387-
<Button asChild variant="outline">
388-
<Link href="/subscriptions/kilo-pass/refer">View referral rewards</Link>
389-
</Button>
390388
{providerManagement.externalManagementAction ? (
391389
<Button asChild variant="outline">
392390
<a

0 commit comments

Comments
 (0)