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),
    },
  }
}