@@ -13,7 +13,6 @@ import { useTRPC } from '@/lib/trpc/utils';
1313import {
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
338337function 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'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'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