File size: 4,564 Bytes
3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 3199d61 d8a167e 3199d61 064bfd6 3199d61 d8a167e 3199d61 064bfd6 3199d61 064bfd6 3199d61 064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import * as React from 'react'
import { useState } from 'react'
import { Text } from '../../ink.js'
import { logEvent } from '../../services/analytics/index.js'
import {
formatGrantAmount,
getCachedOverageCreditGrant,
refreshOverageCreditGrantCache,
} from '../../services/api/overageCreditGrant.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { truncate } from '../../utils/format.js'
import type { FeedConfig } from './Feed.js'
const MAX_IMPRESSIONS = 3
/**
* Whether to show the overage credit upsell on any surface.
*
* Eligibility comes entirely from the backend GET /overage_credit_grant
* response — the CLI doesn't replicate tier/threshold/role checks. The
* backend returns available: false for Team members who aren't admins,
* so they don't see an upsell they can't act on.
*
* isEligibleForOverageCreditGrant — just the backend eligibility. Use for
* persistent reference surfaces (/usage) where the info should show
* whenever eligible, no impression cap.
* shouldShowOverageCreditUpsell — adds the 3-impression cap and
* hasVisitedExtraUsage dismiss. Use for promotional surfaces
* (welcome feed, tips).
*/
export function isEligibleForOverageCreditGrant(): boolean {
const info = getCachedOverageCreditGrant()
if (!info || !info.available || info.granted) return false
return formatGrantAmount(info) !== null
}
export function shouldShowOverageCreditUpsell(): boolean {
if (!isEligibleForOverageCreditGrant()) return false
const config = getGlobalConfig()
if (config.hasVisitedExtraUsage) return false
if ((config.overageCreditUpsellSeenCount ?? 0) >= MAX_IMPRESSIONS)
return false
return true
}
/**
* Kick off a background fetch if the cache is empty. Safe to call
* unconditionally on mount — it no-ops if cache is fresh.
*/
export function maybeRefreshOverageCreditCache(): void {
if (getCachedOverageCreditGrant() !== null) return
void refreshOverageCreditGrantCache()
}
export function useShowOverageCreditUpsell(): boolean {
const [show] = useState(() => {
maybeRefreshOverageCreditCache()
return shouldShowOverageCreditUpsell()
})
return show
}
export function incrementOverageCreditUpsellSeenCount(): void {
let newCount = 0
saveGlobalConfig(prev => {
newCount = (prev.overageCreditUpsellSeenCount ?? 0) + 1
return {
...prev,
overageCreditUpsellSeenCount: newCount,
}
})
logEvent('tengu_overage_credit_upsell_shown', { seen_count: newCount })
}
// Copy from "OC & Bulk Overages copy" doc (#6 — CLI /usage)
function getUsageText(amount: string): string {
return `${amount} in extra usage for third-party apps · /extra-usage`
}
// Copy from "OC & Bulk Overages copy" doc (#4 — CLI Welcome screen).
// Char budgets: title ≤19, subtitle ≤48.
const FEED_SUBTITLE = 'On us. Works on third-party apps · /extra-usage'
function getFeedTitle(amount: string): string {
return `${amount} in extra usage`
}
type Props = { maxWidth?: number; twoLine?: boolean }
export function OverageCreditUpsell({
maxWidth,
twoLine,
}: Props): React.ReactNode {
const info = getCachedOverageCreditGrant()
if (!info) return null
const amount = formatGrantAmount(info)
if (!amount) return null
if (twoLine) {
const title = getFeedTitle(amount)
return (
<>
<Text color="clawd_body">
{maxWidth ? truncate(title, maxWidth) : title}
</Text>
<Text dimColor>
{maxWidth ? truncate(FEED_SUBTITLE, maxWidth) : FEED_SUBTITLE}
</Text>
</>
)
}
const text = getUsageText(amount)
const display = maxWidth ? truncate(text, maxWidth) : text
const highlightLen = Math.min(getFeedTitle(amount).length, display.length)
return (
<Text dimColor>
<Text color="clawd_body">{display.slice(0, highlightLen)}</Text>
{display.slice(highlightLen)}
</Text>
)
}
/**
* Feed config for the homescreen rotating feed. Mirrors
* createGuestPassesFeed in feedConfigs.tsx.
*
* Copy from "OC & Bulk Overages copy" doc (#4 — CLI Welcome screen).
* Char budgets: title ≤19, subtitle ≤48.
*/
export function createOverageCreditFeed(): FeedConfig {
const info = getCachedOverageCreditGrant()
const amount = info ? formatGrantAmount(info) : null
const title = amount ? getFeedTitle(amount) : 'extra usage credit'
return {
title,
lines: [],
customContent: {
content: <Text dimColor>{FEED_SUBTITLE}</Text>,
width: Math.max(title.length, FEED_SUBTITLE.length),
},
}
}
|