feat(billing): implement US-3.7 billing and subscription UX with detailed dashboard, real-time invoice history, inline paywall and upgrade confirmation
This commit is contained in:
@@ -23,6 +23,7 @@ import { useLanguage } from '@/lib/i18n'
|
||||
import { MarkdownContent } from '@/components/markdown-content'
|
||||
import { toast } from 'sonner'
|
||||
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||||
import { InlinePaywall } from './settings/inline-paywall'
|
||||
|
||||
// ── Custom Toast Helper ──────────────────────────────────────────────────────
|
||||
const mToast = {
|
||||
@@ -204,6 +205,11 @@ export function ContextualAIChat({
|
||||
const [chatScope, setChatScope] = useState<'note' | 'all' | string>('note')
|
||||
const [webSearch, setWebSearch] = useState(false)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [quotaExceededFeature, setQuotaExceededFeature] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setQuotaExceededFeature(null)
|
||||
}, [activeTab])
|
||||
|
||||
// Action state
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
@@ -289,12 +295,16 @@ export function ContextualAIChat({
|
||||
const consented = await requestAiConsent()
|
||||
if (!consented) return
|
||||
|
||||
setInput('')
|
||||
try {
|
||||
await sendMessage({ text }, { body: buildChatBody() })
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Chat send error:', error)
|
||||
toast.error(t('chat.assistantError') || 'Failed to send message')
|
||||
const isQuota = error?.status === 402 || (error?.message && error.message.includes('402')) || (error?.message && error.message.includes('quota'));
|
||||
if (isQuota) {
|
||||
setQuotaExceededFeature('chat')
|
||||
} else {
|
||||
toast.error(t('chat.assistantError') || 'Failed to send message')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +327,10 @@ export function ContextualAIChat({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(action.body('', noteImages, language)),
|
||||
})
|
||||
if (res.status === 402) {
|
||||
setQuotaExceededFeature('reformulate')
|
||||
return
|
||||
}
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || t('ai.genericError'))
|
||||
const descs = data.descriptions || []
|
||||
@@ -349,6 +363,10 @@ export function ContextualAIChat({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(action.body(noteContent, undefined, targetLang || language, format)),
|
||||
})
|
||||
if (res.status === 402) {
|
||||
setQuotaExceededFeature('reformulate')
|
||||
return
|
||||
}
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error || t('ai.genericError'))
|
||||
const result = data[action.resultKey] || ''
|
||||
@@ -671,6 +689,14 @@ export function ContextualAIChat({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-0 relative">
|
||||
{quotaExceededFeature && (
|
||||
<div className="absolute inset-x-4 top-4 z-30 animate-in fade-in slide-in-from-top-4 duration-300">
|
||||
<InlinePaywall
|
||||
feature={quotaExceededFeature}
|
||||
onDismiss={() => setQuotaExceededFeature(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{actionPreview && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col bg-[#FDFCFB]/95 dark:bg-[#0D0D0D]/95 backdrop-blur-md animate-in fade-in slide-in-from-top-4 duration-300">
|
||||
<div className="px-6 py-4 border-b border-border flex items-center justify-between shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user