Skip to content

Commit 9b1a470

Browse files
feat(kiloclaw): hide Kilo Pass upsell for subscribed users (#1854)
1 parent 9abaf6e commit 9b1a470

File tree

6 files changed

+482
-222
lines changed

6 files changed

+482
-222
lines changed

src/app/(app)/claw/components/billing/PlanSelectionDialog.tsx

Lines changed: 152 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { useTRPC } from '@/lib/trpc/utils';
1313
import {
1414
COMMIT_PERIOD_MONTHS,
1515
formatMicrodollars,
16-
PLAN_COST_MICRODOLLARS,
1716
PLAN_DISPLAY,
1817
planPriceLabel,
1918
STANDARD_FIRST_MONTH_DOLLARS,
@@ -337,23 +336,26 @@ function CreditsHowItWorks() {
337336

338337
function CreditEnrollmentSection({
339338
selectedPlan,
340-
creditBalanceMicrodollars,
341-
creditIntroEligible,
339+
rawCreditBalanceMicrodollars,
340+
effectiveBalanceMicrodollars,
341+
projectedKiloPassBonusMicrodollars,
342+
costMicrodollars,
342343
onEnroll,
343344
isPending,
344345
}: {
345346
selectedPlan: ClawPlan;
346-
creditBalanceMicrodollars: number;
347-
creditIntroEligible: boolean;
347+
rawCreditBalanceMicrodollars: number;
348+
effectiveBalanceMicrodollars: number;
349+
projectedKiloPassBonusMicrodollars: number;
350+
costMicrodollars: number;
348351
onEnroll: () => void;
349352
isPending: boolean;
350353
}) {
351-
const isIntro = selectedPlan === 'standard' && creditIntroEligible;
352-
const planCost = isIntro
353-
? STANDARD_FIRST_MONTH_MICRODOLLARS
354-
: PLAN_COST_MICRODOLLARS[selectedPlan];
355-
const hasSufficientBalance = creditBalanceMicrodollars >= planCost;
356-
const shortfall = planCost - creditBalanceMicrodollars;
354+
const isIntro =
355+
selectedPlan === 'standard' && costMicrodollars === STANDARD_FIRST_MONTH_MICRODOLLARS;
356+
const hasProjectedBonus = projectedKiloPassBonusMicrodollars > 0;
357+
const hasSufficientBalance = effectiveBalanceMicrodollars >= costMicrodollars;
358+
const shortfall = Math.max(0, costMicrodollars - effectiveBalanceMicrodollars);
357359
const planLabel = selectedPlan === 'commit' ? 'Commit' : 'Standard';
358360
const priceLabel = isIntro
359361
? `$${STANDARD_FIRST_MONTH_DOLLARS} first month, then ${planPriceLabel(selectedPlan)}`
@@ -369,16 +371,27 @@ function CreditEnrollmentSection({
369371
<p className="text-muted-foreground mb-1 text-sm">
370372
{planLabel} Plan — {priceLabel} from your credit balance
371373
</p>
372-
<p className="mb-3 text-xs text-emerald-400/80">
373-
Balance: {formatMicrodollars(creditBalanceMicrodollars)}
374-
</p>
374+
{hasProjectedBonus ? (
375+
<div className="mb-3 space-y-1 text-xs text-emerald-400/80">
376+
<p>Current balance: {formatMicrodollars(rawCreditBalanceMicrodollars)}</p>
377+
<p>
378+
Projected Kilo Pass bonus for this charge:{' '}
379+
{formatMicrodollars(projectedKiloPassBonusMicrodollars)}
380+
</p>
381+
<p>Effective balance: {formatMicrodollars(effectiveBalanceMicrodollars)}</p>
382+
</div>
383+
) : (
384+
<p className="mb-3 text-xs text-emerald-400/80">
385+
Balance: {formatMicrodollars(rawCreditBalanceMicrodollars)}
386+
</p>
387+
)}
375388
<Button
376389
onClick={onEnroll}
377390
disabled={isPending}
378391
variant="primary"
379392
className="w-full py-3 font-semibold"
380393
>
381-
{isPending ? 'Activating…' : `Pay ${formatMicrodollars(planCost)} with Credits`}
394+
{isPending ? 'Activating…' : `Pay ${formatMicrodollars(costMicrodollars)} with Credits`}
382395
</Button>
383396
</div>
384397
);
@@ -393,13 +406,31 @@ function CreditEnrollmentSection({
393406
<div className="text-muted-foreground space-y-1 text-sm">
394407
<div className="flex justify-between">
395408
<span>Balance</span>
396-
<span className="text-foreground">{formatMicrodollars(creditBalanceMicrodollars)}</span>
409+
<span className="text-foreground">
410+
{formatMicrodollars(rawCreditBalanceMicrodollars)}
411+
</span>
397412
</div>
413+
{hasProjectedBonus && (
414+
<div className="flex justify-between">
415+
<span>Projected Kilo Pass bonus</span>
416+
<span className="text-foreground">
417+
{formatMicrodollars(projectedKiloPassBonusMicrodollars)}
418+
</span>
419+
</div>
420+
)}
421+
{hasProjectedBonus && (
422+
<div className="flex justify-between">
423+
<span>Effective balance</span>
424+
<span className="text-foreground">
425+
{formatMicrodollars(effectiveBalanceMicrodollars)}
426+
</span>
427+
</div>
428+
)}
398429
<div className="flex justify-between">
399430
<span>
400431
{planLabel} plan cost{isIntro ? ' (first month)' : ''}
401432
</span>
402-
<span className="text-foreground">{formatMicrodollars(planCost)}</span>
433+
<span className="text-foreground">{formatMicrodollars(costMicrodollars)}</span>
403434
</div>
404435
<div className="flex justify-between border-t border-amber-500/20 pt-1 font-medium text-amber-400">
405436
<span>Shortfall</span>
@@ -455,6 +486,13 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
455486
const creditBalance = billing?.creditBalanceMicrodollars ?? null;
456487
const hasCredits = creditBalance !== null && creditBalance > 0;
457488
const creditIntroEligible = billing?.creditIntroEligible ?? false;
489+
const hasActiveKiloPass = billing?.hasActiveKiloPass ?? false;
490+
const creditEnrollmentPreview = billing?.creditEnrollmentPreview ?? null;
491+
const showKiloPassUpsell = !hasActiveKiloPass;
492+
const selectedCreditEnrollmentPreview =
493+
hostingOnlyPlan !== null ? (creditEnrollmentPreview?.[hostingOnlyPlan] ?? null) : null;
494+
const showCreditEnrollment =
495+
!!selectedCreditEnrollmentPreview && (hasCredits || hasActiveKiloPass);
458496

459497
const hostingOnlyActive = hostingOnlyPlan !== null;
460498
const commitDisabled = !isCommitAvailable(selectedTier, cadence);
@@ -534,6 +572,11 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
534572
const hostingOnlyLabel = hostingOnlyPlan
535573
? `Subscribe to ${hostingOnlyPlan === 'commit' ? 'Commit' : 'Standard'} Plan – $${hostingOnlyPlan === 'commit' ? PLAN_DISPLAY.commit.totalDollars : PLAN_DISPLAY.standard.monthlyDollars}`
536574
: 'Select a plan above';
575+
const hostingSectionTitle = hasActiveKiloPass ? 'Choose a Hosting Plan' : 'Hosting Only';
576+
const hostingSectionDescription = hasActiveKiloPass
577+
? 'You already have Kilo Pass. Choose a hosting plan and fund it from your credit balance or pay directly via Stripe.'
578+
: 'Pay for hosting directly via Stripe. You can buy credits separately for AI inference.';
579+
const highlightHostingSection = hasActiveKiloPass || hostingOnlyActive;
537580

538581
return (
539582
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -548,102 +591,104 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
548591
</p>
549592
</div>
550593

551-
{/* Section 1: Kilo Pass (Recommended) */}
552-
<div
553-
className={cn(
554-
'rounded-xl border p-5 transition-all',
555-
'border-blue-500/30 bg-gradient-to-b from-blue-500/[0.04] to-transparent shadow-[0_0_0_1px_rgba(59,130,246,0.08)]'
556-
)}
557-
>
558-
<div className="mb-3 flex items-center gap-2.5">
559-
<Crown className="h-5 w-5 text-amber-400" />
560-
<h3 className="flex-1 text-base font-semibold">Activate with Kilo Pass</h3>
561-
<Badge className="rounded-full bg-blue-500/15 px-2.5 py-0.5 text-[11px] font-semibold text-blue-300 ring-1 ring-blue-500/30 border-transparent">
562-
Recommended
563-
</Badge>
564-
</div>
565-
566-
<p className="text-muted-foreground mb-4 text-[13px]">
567-
Credits for KiloClaw hosting + AI inference. Earn up to{' '}
568-
<span className="text-emerald-300">50% free bonus credits</span>.
569-
</p>
570-
571-
<CadenceToggle cadence={cadence} onChange={handleCadenceChange} />
572-
573-
{/* Tier cards */}
574-
<div className="mb-3.5 grid grid-cols-1 gap-2 sm:grid-cols-3 sm:gap-2.5">
575-
{TIERS.map(tier => (
576-
<TierCard
577-
key={tier}
578-
tier={tier}
579-
cadence={cadence}
580-
isSelected={selectedTier === tier}
581-
onSelect={() => handleTierSelect(tier)}
582-
/>
583-
))}
584-
</div>
585-
586-
<CreditsHowItWorks />
594+
{showKiloPassUpsell && (
595+
<>
596+
{/* Section 1: Kilo Pass (Recommended) */}
597+
<div
598+
className={cn(
599+
'rounded-xl border p-5 transition-all',
600+
'border-blue-500/30 bg-gradient-to-b from-blue-500/[0.04] to-transparent shadow-[0_0_0_1px_rgba(59,130,246,0.08)]'
601+
)}
602+
>
603+
<div className="mb-3 flex items-center gap-2.5">
604+
<Crown className="h-5 w-5 text-amber-400" />
605+
<h3 className="flex-1 text-base font-semibold">Activate with Kilo Pass</h3>
606+
<Badge className="rounded-full border-transparent bg-blue-500/15 px-2.5 py-0.5 text-[11px] font-semibold text-blue-300 ring-1 ring-blue-500/30">
607+
Recommended
608+
</Badge>
609+
</div>
587610

588-
{/* Warning */}
589-
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5">
590-
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
591-
<p className="text-xs leading-relaxed text-amber-300">
592-
Kilo Pass is a <strong className="font-normal">credits subscription</strong>.
593-
Hosting is charged as a credit deduction from your balance. Cancelling Kilo Pass
594-
does <strong className="font-normal">not</strong> cancel KiloClaw hosting.
595-
</p>
596-
</div>
611+
<p className="text-muted-foreground mb-4 text-[13px]">
612+
Credits for KiloClaw hosting + AI inference. Earn up to{' '}
613+
<span className="text-emerald-300">50% free bonus credits</span>.
614+
</p>
615+
616+
<CadenceToggle cadence={cadence} onChange={handleCadenceChange} />
617+
618+
{/* Tier cards */}
619+
<div className="mb-3.5 grid grid-cols-1 gap-2 sm:grid-cols-3 sm:gap-2.5">
620+
{TIERS.map(tier => (
621+
<TierCard
622+
key={tier}
623+
tier={tier}
624+
cadence={cadence}
625+
isSelected={selectedTier === tier}
626+
onSelect={() => handleTierSelect(tier)}
627+
/>
628+
))}
629+
</div>
597630

598-
{/* Hosting plan radios */}
599-
{selectedTier && (
600-
<HostingRadioGroup
601-
hostingPlan={hostingPlan}
602-
onSelect={handleHostingPlanSelect}
603-
commitDisabled={commitDisabled}
604-
creditIntroEligible={creditIntroEligible}
605-
/>
606-
)}
631+
<CreditsHowItWorks />
607632

608-
<Button
609-
onClick={handleKiloPassCheckout}
610-
disabled={!kiloPassReady || kiloPassUpsell.isPending}
611-
variant="primary"
612-
className="w-full py-3.5 text-base font-semibold"
613-
>
614-
{kiloPassUpsell.isPending ? 'Redirecting to Stripe…' : kiloPassButtonLabel}
615-
</Button>
616-
<p className="text-muted-foreground mt-2 text-center text-xs">
617-
You&apos;ll be redirected to Stripe to pay for Kilo Pass
618-
</p>
619-
</div>
633+
{/* Warning */}
634+
<div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2.5">
635+
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
636+
<p className="text-xs leading-relaxed text-amber-300">
637+
Kilo Pass is a <strong className="font-normal">credits subscription</strong>.
638+
Hosting is charged as a credit deduction from your balance. Cancelling Kilo Pass
639+
does <strong className="font-normal">not</strong> cancel KiloClaw hosting.
640+
</p>
641+
</div>
620642

621-
{/* Divider */}
622-
<div className="flex items-center gap-3">
623-
<div className="bg-border h-px flex-1" />
624-
<span className="text-muted-foreground text-xs uppercase tracking-wider">
625-
or pay for KiloClaw hosting only
626-
</span>
627-
<div className="bg-border h-px flex-1" />
628-
</div>
643+
{/* Hosting plan radios */}
644+
{selectedTier && (
645+
<HostingRadioGroup
646+
hostingPlan={hostingPlan}
647+
onSelect={handleHostingPlanSelect}
648+
commitDisabled={commitDisabled}
649+
creditIntroEligible={creditIntroEligible}
650+
/>
651+
)}
652+
653+
<Button
654+
onClick={handleKiloPassCheckout}
655+
disabled={!kiloPassReady || kiloPassUpsell.isPending}
656+
variant="primary"
657+
className="w-full py-3.5 text-base font-semibold"
658+
>
659+
{kiloPassUpsell.isPending ? 'Redirecting to Stripe…' : kiloPassButtonLabel}
660+
</Button>
661+
<p className="text-muted-foreground mt-2 text-center text-xs">
662+
You&apos;ll be redirected to Stripe to pay for Kilo Pass
663+
</p>
664+
</div>
665+
666+
{/* Divider */}
667+
<div className="flex items-center gap-3">
668+
<div className="bg-border h-px flex-1" />
669+
<span className="text-muted-foreground text-xs uppercase tracking-wider">
670+
or pay for KiloClaw hosting only
671+
</span>
672+
<div className="bg-border h-px flex-1" />
673+
</div>
674+
</>
675+
)}
629676

630-
{/* Section 2: Hosting Only (Secondary) */}
677+
{/* Section 2: Hosting selection / standalone checkout */}
631678
<div
632679
className={cn(
633680
'rounded-xl border p-5 transition-all',
634-
hostingOnlyActive
681+
highlightHostingSection
635682
? 'border-border opacity-100'
636683
: 'border-border/50 opacity-70 hover:opacity-90 hover:border-border'
637684
)}
638685
>
639686
<div className="mb-3 flex items-center gap-2.5">
640687
<Server className="text-muted-foreground h-[18px] w-[18px]" />
641-
<h3 className="text-base font-semibold">Hosting Only</h3>
688+
<h3 className="text-base font-semibold">{hostingSectionTitle}</h3>
642689
</div>
643690

644-
<p className="text-muted-foreground mb-3.5 text-[13px]">
645-
Pay for hosting directly via Stripe. You can buy credits separately for AI inference.
646-
</p>
691+
<p className="text-muted-foreground mb-3.5 text-[13px]">{hostingSectionDescription}</p>
647692

648693
<div className="mb-4 grid grid-cols-2 gap-2">
649694
<HostingOnlyPlanCard
@@ -660,13 +705,19 @@ export function PlanSelectionDialog({ open, onOpenChange }: PlanSelectionDialogP
660705
/>
661706
</div>
662707

663-
{/* Credit enrollment option — shown when user has credits and a hosting-only plan is selected */}
664-
{hasCredits && hostingOnlyPlan && (
708+
{/* Credit enrollment option — shown when a hosting plan is selected and credits are available or Kilo Pass is active */}
709+
{showCreditEnrollment && hostingOnlyPlan && (
665710
<>
666711
<CreditEnrollmentSection
667712
selectedPlan={hostingOnlyPlan}
668-
creditBalanceMicrodollars={creditBalance}
669-
creditIntroEligible={creditIntroEligible}
713+
rawCreditBalanceMicrodollars={creditBalance ?? 0}
714+
effectiveBalanceMicrodollars={
715+
selectedCreditEnrollmentPreview.effectiveBalanceMicrodollars
716+
}
717+
projectedKiloPassBonusMicrodollars={
718+
selectedCreditEnrollmentPreview.projectedKiloPassBonusMicrodollars
719+
}
720+
costMicrodollars={selectedCreditEnrollmentPreview.costMicrodollars}
670721
onEnroll={handleEnrollWithCredits}
671722
isPending={enrollWithCredits.isPending}
672723
/>

0 commit comments

Comments
 (0)