[web] Delay suggest fix paywall until suggest button has been clicked (#33458)

GitOrigin-RevId: 11d2ec0c9c33aea3fedff57d5f1a74d6ce774017
This commit is contained in:
Mathias Jakobsen
2026-05-06 19:44:45 +01:00
committed by Copybot
parent ff6ad4b41e
commit 76fbb56107
4 changed files with 147 additions and 6 deletions

View File

@@ -0,0 +1,30 @@
import { useCallback, useEffect, useState } from 'react'
import AiPaywallNotification from '@/shared/components/ai-paywall-notification'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import useEventListener from '@/shared/hooks/use-event-listener'
export default function ErrorAssistantAiPaywallNotification() {
const { showLogs } = useCompileContext()
const [hasTriggered, setHasTriggered] = useState(false)
useEventListener(
'aiAssist:showPaywall',
useCallback((event: CustomEvent<{ origin?: string }>) => {
if (event.detail?.origin === 'suggest-fix') {
setHasTriggered(true)
}
}, [])
)
useEffect(() => {
if (!showLogs) {
setHasTriggered(false)
}
}, [showLogs])
if (!hasTriggered) {
return null
}
return <AiPaywallNotification featureLocation="errorAssist" />
}

View File

@@ -16,7 +16,7 @@ import getMeta from '@/utils/meta'
import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button' import PdfClearCacheButton from '@/features/pdf-preview/components/pdf-clear-cache-button'
import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button' import PdfDownloadFilesButton from '@/features/pdf-preview/components/pdf-download-files-button'
import RollingBuildSelectedReminder from './rolling-build-selected-reminder' import RollingBuildSelectedReminder from './rolling-build-selected-reminder'
import AiPaywallNotification from '@/shared/components/ai-paywall-notification' import ErrorAssistantAiPaywallNotification from './error-assistant-ai-paywall-notification'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
// todo: quota clean-up remove unneeded old paywall component // todo: quota clean-up remove unneeded old paywall component
@@ -84,7 +84,7 @@ function ErrorLogs({
{logsComponents.map(({ import: { default: Component }, path }) => ( {logsComponents.map(({ import: { default: Component }, path }) => (
<Component key={path} /> <Component key={path} />
))} ))}
<AiPaywallNotification featureLocation="errorAssist" /> <ErrorAssistantAiPaywallNotification />
<TabContent className="error-logs new-error-logs"> <TabContent className="error-logs new-error-logs">
<div className="logs-pane-content"> <div className="logs-pane-content">
<RollingBuildSelectedReminder /> <RollingBuildSelectedReminder />

View File

@@ -0,0 +1,103 @@
import { FC, PropsWithChildren, useState } from 'react'
import { DetachCompileContext } from '@/shared/context/detach-compile-context'
import ErrorAssistantAiPaywallNotification from '@/features/pdf-preview/components/error-assistant-ai-paywall-notification'
import {
EditorProviders,
makeEditorProvider,
} from '../../helpers/editor-providers'
const PAYWALL_TEXT = 'Youve reached the fair usage limit on your plan'
const futureDate = () => new Date(Date.now() + 60 * 60 * 1000)
function dispatchSuggestFixPaywall() {
cy.window().then(win => {
win.dispatchEvent(
new CustomEvent('aiAssist:showPaywall', {
detail: { origin: 'suggest-fix' },
})
)
})
}
function makeShowLogsCompileProvider(initialShowLogs: boolean) {
const Provider: FC<PropsWithChildren> = ({ children }) => {
const [showLogs, setShowLogs] = useState(initialShowLogs)
return (
<DetachCompileContext.Provider value={{ showLogs, setShowLogs } as any}>
<button
type="button"
data-testid="toggle-logs"
onClick={() => setShowLogs(prev => !prev)}
>
toggle logs
</button>
{children}
</DetachCompileContext.Provider>
)
}
return Provider
}
function mountSuggestFixPaywall(initialShowLogs = true) {
cy.window().then(win => {
win.metaAttributesCache.set('ol-showAiFeatures', true)
})
cy.mount(
<EditorProviders
features={{ aiErrorAssistant: true }}
providers={{
EditorProvider: makeEditorProvider({
hasSuggestionsLeft: false,
premiumSuggestionResetDate: futureDate(),
}),
DetachCompileProvider: makeShowLogsCompileProvider(initialShowLogs),
}}
>
<ErrorAssistantAiPaywallNotification />
</EditorProviders>
)
}
describe('<ErrorAssistantAiPaywallNotification />', function () {
it('does not render the paywall before the suggest-fix paywall event fires', function () {
mountSuggestFixPaywall()
cy.contains(PAYWALL_TEXT).should('not.exist')
})
it('renders the paywall after the suggest-fix paywall event fires', function () {
mountSuggestFixPaywall()
dispatchSuggestFixPaywall()
cy.contains(PAYWALL_TEXT).should('be.visible')
})
it('ignores paywall events from other origins', function () {
mountSuggestFixPaywall()
cy.window().then(win => {
win.dispatchEvent(
new CustomEvent('aiAssist:showPaywall', {
detail: { origin: 'workbench' },
})
)
})
cy.contains(PAYWALL_TEXT).should('not.exist')
})
it('hides the paywall when the logs panel is closed', function () {
mountSuggestFixPaywall()
dispatchSuggestFixPaywall()
cy.contains(PAYWALL_TEXT).should('be.visible')
cy.findByTestId('toggle-logs').click()
cy.contains(PAYWALL_TEXT).should('not.exist')
})
it('does not re-show the paywall when the logs panel is reopened', function () {
mountSuggestFixPaywall()
dispatchSuggestFixPaywall()
cy.findByTestId('toggle-logs').click()
cy.findByTestId('toggle-logs').click()
cy.contains(PAYWALL_TEXT).should('not.exist')
})
})

View File

@@ -277,23 +277,31 @@ export function makeEditorProvider({
cobranding = undefined, cobranding = undefined,
renameProject = () => {}, renameProject = () => {},
isRestrictedTokenMember, isRestrictedTokenMember,
hasSuggestionsLeft = false,
hasTokensLeft = false,
premiumSuggestionResetDate = new Date(),
tokenResetDate = new Date(),
}: { }: {
isProjectOwner?: boolean isProjectOwner?: boolean
cobranding?: Cobranding cobranding?: Cobranding
renameProject?: () => void renameProject?: () => void
isRestrictedTokenMember?: boolean isRestrictedTokenMember?: boolean
hasSuggestionsLeft?: boolean
hasTokensLeft?: boolean
premiumSuggestionResetDate?: Date
tokenResetDate?: Date
} = {}) { } = {}) {
const EditorProvider: FC<PropsWithChildren> = ({ children }) => { const EditorProvider: FC<PropsWithChildren> = ({ children }) => {
const value = { const value = {
isProjectOwner, isProjectOwner,
renameProject, renameProject,
isPendingEditor: false, isPendingEditor: false,
hasSuggestionsLeft: false, hasSuggestionsLeft,
premiumSuggestionResetDate: new Date(), premiumSuggestionResetDate,
hasTokensLeft: false, hasTokensLeft,
tokensLeft: 0, tokensLeft: 0,
setTokensLeft: () => {}, setTokensLeft: () => {},
tokenResetDate: new Date(), tokenResetDate,
setTokenResetDate: () => {}, setTokenResetDate: () => {},
suggestionsLeft: 0, suggestionsLeft: 0,
setSuggestionsLeft: () => {}, setSuggestionsLeft: () => {},