diff --git a/services/web/app/src/Features/Institutions/InstitutionsFeatures.mjs b/services/web/app/src/Features/Institutions/InstitutionsFeatures.mjs index f0dba423d9..7b9119a1de 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsFeatures.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsFeatures.mjs @@ -15,14 +15,7 @@ async function _getInstitutionsAddons(userId) { const hasAssistBundle = affiliates.some( affiliate => affiliate?.institution?.writefullCommonsAccount === true ) - - // todo: seperate quota value depending on source of entitlement if needed - // todo: quota clean-up: remove aiErrorAssistant once migration finishes - const bundleFeatures = { - aiUsageQuota: Settings.writefull.quotaTierGranted, - aiErrorAssistant: true, - } - return hasAssistBundle ? bundleFeatures : {} + return hasAssistBundle ? { aiErrorAssistant: true } : {} } async function getInstitutionsFeatures(userId) { diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 18a3288d0b..6e43c54f8a 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -952,8 +952,6 @@ const _ProjectController = { symbolPaletteAvailable: Features.hasFeature('symbol-palette'), userRestrictions: Array.from(req.userRestrictions || []), showAiFeatures: aiFeaturesAllowed && !aiFeaturesDisabled, - onAiFreeTrial: - user.features?.aiUsageQuota === Settings.aiFeatures?.freeTrialQuota, detachRole, metadata: { viewport: false }, showUpgradePrompt, diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index 971620f118..94355cc274 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -510,10 +510,8 @@ async function projectListPage(req, res, next) { logger.error({ err: error }, 'Failed to get individual subscription') } - const aiBlocked = - Features.hasFeature('saas') && !(await _canUseAIAssist(user)) - const hasAiAssist = - Features.hasFeature('saas') && (await _userHasAIAssist(user)) + const aiBlocked = !(await _canUseAIAssist(user)) + const hasAiAssist = await _userHasAIAssist(user) await SplitTestHandler.promises.getAssignment( req, @@ -901,13 +899,11 @@ function _hasActiveFilter(filters) { ) } -// todo: quota clean-up: rename function and vars async function _userHasAIAssist(user) { - // Check if the user has a non free trial version of our AI features - if (user.features?.aiUsageQuota === Settings.aiFeatures.unlimitedQuota) { + // Check if the user has AI Assist enabled via Overleaf + if (user.features?.aiErrorAssistant) { return true } - // Check if the user has AI Assist enabled via Writefull const { isPremium: hasAiAssistViaWritefull } = await UserGetter.promises.getWritefullData(user._id) @@ -922,7 +918,6 @@ async function _userHasAIAssist(user) { // It does NOT determine if the user has AI Assist enabled async function _canUseAIAssist(user) { // Check if the assistant has been manually disabled by the user - // post https://github.com/overleaf/internal/pull/31273 we can rely on user.aiFeatures being populated if (user.aiFeatures?.enabled === false) { return false } diff --git a/services/web/app/src/Features/SplitTests/SplitTestHandler.mjs b/services/web/app/src/Features/SplitTests/SplitTestHandler.mjs index c333511d03..6447f9d4e5 100644 --- a/services/web/app/src/Features/SplitTests/SplitTestHandler.mjs +++ b/services/web/app/src/Features/SplitTests/SplitTestHandler.mjs @@ -306,20 +306,6 @@ async function getOneTimeAssignment(splitTestName) { } } -/** - * Checks if a feature flag is enabled for a specific user - * - * Retrieves the feature flag assignment for a user and determines if the assigned variant is 'enabled' - * - * @param {string} userId - The ID of the user to check the feature flag for - * @param {string} splitTestName - The unique name of the feature flag - * @returns {Promise} True if the user's assigned variant is 'enabled', false otherwise - */ -async function featureFlagEnabledForUser(userId, splitTestName) { - const { variant } = await getAssignmentForUser(userId, splitTestName) - return variant === 'enabled' -} - /** * Returns an array of valid variant names for the given split test, including default * @@ -816,7 +802,6 @@ export default { getPercentile, getAssignment: callbackify(getAssignment), getAssignmentForUser: callbackify(getAssignmentForUser), - featureFlagEnabledForUser: callbackify(featureFlagEnabledForUser), getOneTimeAssignment: callbackify(getOneTimeAssignment), getActiveAssignmentsForUser: callbackify(getActiveAssignmentsForUser), hasUserBeenAssignedToVariant: callbackify(hasUserBeenAssignedToVariant), @@ -825,7 +810,6 @@ export default { promises: { getAssignment, getAssignmentForUser, - featureFlagEnabledForUser, getOneTimeAssignment, getActiveAssignmentsForUser, hasUserBeenAssignedToVariant, diff --git a/services/web/app/src/Features/Subscription/FeaturesHelper.mjs b/services/web/app/src/Features/Subscription/FeaturesHelper.mjs index 312d71898c..a70a903f29 100644 --- a/services/web/app/src/Features/Subscription/FeaturesHelper.mjs +++ b/services/web/app/src/Features/Subscription/FeaturesHelper.mjs @@ -38,15 +38,6 @@ function mergeFeatures(featuresA, featuresB) { features.compileTimeout || 0, featuresB.compileTimeout || 0 ) - } else if (key === 'aiUsageQuota') { - if ( - features.aiUsageQuota === Settings.aiFeatures.unlimitedQuota || - featuresB.aiUsageQuota === Settings.aiFeatures.unlimitedQuota - ) { - features.aiUsageQuota = Settings.aiFeatures.unlimitedQuota - } else { - features.aiUsageQuota = Settings.aiFeatures.freeTrialQuota - } } else { // Boolean keys, true is better features[key] = features[key] || featuresB[key] diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs index 4a7b519b98..b1ca1c27cb 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs @@ -100,10 +100,7 @@ async function refreshFeatures(userId, reason) { }, json: { userOverleafId: userId, - // todo: quota clean-up: collab with writefull to rename this, and check if still needed - hasAiAssist: - newFeatures.aiErrorAssistant || - newFeatures.aiUsageQuota === Settings.aiFeatures.unlimitedQuota, + hasAiAssist: newFeatures.aiErrorAssistant, }, method: 'POST', } @@ -195,9 +192,6 @@ async function _getIndividualFeatures(userId) { featureSets.push(_subscriptionToFeatures(subscription)) } - // todo: quota clean-up - remove - // if they are in the quota split test, we no longer look at the add-on, since every plan will now have the same quota - // standalone plan will receive correct state since their plan will provide the correct quota featureSets.push(_aiAddOnFeatures(subscription)) return _.reduce(featureSets, FeaturesHelper.mergeFeatures, {}) } @@ -243,14 +237,9 @@ function _subscriptionToFeatures(subscription) { } } -// todo: quota clean-up: remove post split test function _aiAddOnFeatures(subscription) { if (subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)) { - return { - // allow both naming systems to work - aiErrorAssistant: true, - aiUsageQuota: Settings.aiFeatures.unlimitedQuota, - } + return { aiErrorAssistant: true } } else { return {} } diff --git a/services/web/app/src/infrastructure/FeatureUsageRateLimiter.mjs b/services/web/app/src/infrastructure/FeatureUsageRateLimiter.mjs index f067366a81..4febfbd069 100644 --- a/services/web/app/src/infrastructure/FeatureUsageRateLimiter.mjs +++ b/services/web/app/src/infrastructure/FeatureUsageRateLimiter.mjs @@ -87,6 +87,7 @@ export default class FeatureUsageRateLimiter { upsert: true, } ).exec() + const featureUsage = featureUsages.features?.[this.featureName] ?? {} setRateLimitHeaders(res, featureUsage, allowance) this._checkRateLimit(featureUsage, allowance) @@ -184,6 +185,7 @@ function setRateLimitHeaders(res, featureUsage, allowance) { const usage = featureUsage.usage ?? 0 const refreshEpoch = periodStart.getTime() + PERIOD_IN_MILLISECONDS const secondsTillReset = Math.ceil((refreshEpoch - Date.now()) / 1000) + if (!res.headersSent) { res.set('RateLimit-Limit', String(allowance)) res.set('RateLimit-Remaining', String(Math.max(0, allowance - usage))) diff --git a/services/web/app/src/models/User.mjs b/services/web/app/src/models/User.mjs index 6a7b169a21..454d378622 100644 --- a/services/web/app/src/models/User.mjs +++ b/services/web/app/src/models/User.mjs @@ -143,7 +143,6 @@ export const UserSchema = new Schema( type: Boolean, default: false, }, - aiUsageQuota: { type: String, default: 'basic' }, }, featuresOverrides: [ { @@ -156,9 +155,7 @@ export const UserSchema = new Schema( expiresAt: { type: Date }, note: { type: String }, features: { - // todo: quota clean-up: remove aiErrorAssistant aiErrorAssistant: { type: Boolean }, - aiUsageQuota: { type: String }, collaborators: { type: Number }, versioning: { type: Boolean }, dropbox: { type: Boolean }, diff --git a/services/web/app/views/project/editor/_meta.pug b/services/web/app/views/project/editor/_meta.pug index 3bb32ac74b..2aa4ca617a 100644 --- a/services/web/app/views/project/editor/_meta.pug +++ b/services/web/app/views/project/editor/_meta.pug @@ -25,7 +25,6 @@ meta(name="ol-debugPdfDetach" data-type="boolean" content=debugPdfDetach) meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette) meta(name="ol-symbolPaletteAvailable" data-type="boolean" content=symbolPaletteAvailable) meta(name="ol-showAiFeatures" data-type="boolean" content=showAiFeatures) -meta(name="ol-onAiFreeTrial" data-type="boolean" content=onAiFreeTrial) meta(name="ol-detachRole" data-type="string" content=detachRole) meta(name="ol-imageNames" data-type="json" content=imageNames) meta(name="ol-languages" data-type="json" content=languages) diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index d84611c5fa..f853232fdf 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -419,11 +419,6 @@ module.exports = { personal: defaultFeatures, }, - aiFeatures: { - freeTrialQuota: 'basic', - unlimitedQuota: 'unlimited', - }, - groupPlanModalOptions: { plan_codes: [], currencies: [], diff --git a/services/web/frontend/js/shared/context/user-features-context.tsx b/services/web/frontend/js/shared/context/user-features-context.tsx index 151ca3357a..a162ae0540 100644 --- a/services/web/frontend/js/shared/context/user-features-context.tsx +++ b/services/web/frontend/js/shared/context/user-features-context.tsx @@ -11,12 +11,9 @@ import { useUserContext } from './user-context' import { useReceiveUser } from '../hooks/user-channel/use-receive-user' import { getJSON } from '@/infrastructure/fetch-json' import { useEditorContext } from './editor-context' -import getMeta from '@/utils/meta' export const UserFeaturesContext = createContext(undefined) -const onAiFreeTrial = getMeta('ol-onAiFreeTrial') - export const UserFeaturesProvider: FC = ({ children, }) => { @@ -34,12 +31,7 @@ export const UserFeaturesProvider: FC = ({ useEffect(() => { const listener = async ({ isPremium }: { isPremium: boolean }) => { - // todo: quota clean-up: remove once we are transitioned off aiErrorAssistant naming - const hasPremiumQuota = !onAiFreeTrial - const alreadyPremium = - features?.aiErrorAssistant === isPremium || - hasPremiumQuota === isPremium - if (alreadyPremium) { + if (features?.aiErrorAssistant === isPremium) { // the user is premium on writefull and has the AI assist, no need to refresh the features return } diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 88bd544a36..fa6ddb6e46 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -213,7 +213,6 @@ export interface Meta { 'ol-notificationsInstitution': InstitutionType[] 'ol-oauthProviders': OAuthProviders 'ol-odcData': OnboardingFormData - 'ol-onAiFreeTrial': boolean 'ol-otMigrationStage': number 'ol-overallThemes': OverallThemeMeta[] 'ol-ownerIsManaged': boolean diff --git a/services/web/test/acceptance/config/settings.test.defaults.js b/services/web/test/acceptance/config/settings.test.defaults.js index b9123cec8b..860b5e2066 100644 --- a/services/web/test/acceptance/config/settings.test.defaults.js +++ b/services/web/test/acceptance/config/settings.test.defaults.js @@ -115,7 +115,6 @@ module.exports = { compileGroup: 'standard', trackChanges: false, symbolPalette: false, - aiUsageQuota: 'basic', aiErrorAssistant: false, }, personal: { @@ -133,7 +132,6 @@ module.exports = { compileGroup: 'standard', trackChanges: false, symbolPalette: false, - aiUsageQuota: 'basic', aiErrorAssistant: false, }, collaborator: { @@ -151,7 +149,6 @@ module.exports = { compileGroup: 'priority', trackChanges: true, symbolPalette: true, - aiUsageQuota: 'basic', aiErrorAssistant: false, }, professional: { @@ -169,7 +166,6 @@ module.exports = { compileGroup: 'priority', trackChanges: true, symbolPalette: true, - aiUsageQuota: 'basic', aiErrorAssistant: false, }, }), diff --git a/services/web/test/unit/src/Institutions/InstitutionsFeatures.test.mjs b/services/web/test/unit/src/Institutions/InstitutionsFeatures.test.mjs index 5c7264d0ee..abd608a476 100644 --- a/services/web/test/unit/src/Institutions/InstitutionsFeatures.test.mjs +++ b/services/web/test/unit/src/Institutions/InstitutionsFeatures.test.mjs @@ -30,13 +30,6 @@ describe('InstitutionsFeatures', function () { default: { institutionPlanCode: ctx.institutionPlanCode, overleaf: {}, - writefull: { - quotaTierGranted: 'unlimited', - }, - aiFeatures: { - freeTrialQuota: 'basic', - unlimitedQuota: 'unlimited', - }, }, })) @@ -59,18 +52,10 @@ describe('InstitutionsFeatures', function () { } ctx.testFeatures = { features: { institution: 'all' } } ctx.testFeaturesWithAiAddon = { - features: { - institution: 'all', - aiUsageQuota: 'unlimited', - aiErrorAssistant: true, - }, + features: { institution: 'all', aiErrorAssistant: true }, } ctx.testFeaturesWithNoAddon = { - features: { - institution: 'all', - aiUsageQuota: 'basic', - aiErrorAssistant: false, - }, + features: { institution: 'all', aiErrorAssistant: false }, } }) diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index 87744d40ec..706f808c83 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -40,7 +40,6 @@ describe('ProjectListController', function () { theme: 'textmate', mode: 'none', }, - aiFeatures: { enabled: false }, } ctx.users = { 'user-1': { diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs index f1ee3e1f34..e17011ac2c 100644 --- a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs +++ b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs @@ -74,11 +74,6 @@ describe('FeaturesUpdater', function () { }, writefull: { overleafApiUrl: 'https://www.writefull.com', - quotaTierGranted: 'unlimited', - }, - aiFeatures: { - freeTrialQuota: 'basic', - unlimitedQuota: 'unlimited', }, } @@ -328,7 +323,6 @@ describe('FeaturesUpdater', function () { default: 'features', individual: 'features', aiErrorAssistant: true, - aiUsageQuota: 'unlimited', }) }) }) @@ -347,7 +341,6 @@ describe('FeaturesUpdater', function () { expect(features).to.deep.equal({ default: 'features', aiErrorAssistant: true, - aiUsageQuota: 'unlimited', }) }) }) diff --git a/services/web/test/unit/src/User/UserGetter.test.mjs b/services/web/test/unit/src/User/UserGetter.test.mjs index 8e738643f6..8299dac405 100644 --- a/services/web/test/unit/src/User/UserGetter.test.mjs +++ b/services/web/test/unit/src/User/UserGetter.test.mjs @@ -66,10 +66,6 @@ describe('UserGetter', function () { vi.doMock('@overleaf/settings', () => ({ default: (ctx.settings = { reconfirmNotificationDays: 14, - aiFeatures: { - freeTrialQuota: 'basic', - unlimitedQuota: 'unlimited', - }, }), })) @@ -1316,8 +1312,8 @@ describe('UserGetter', function () { it('should take into account features overrides from modules', async function (ctx) { // this case occurs when the user has bought the ai bundle on WF, which should include our error assistant - const bundleFeatures = { aiUsageQuota: 'unlimited' } - ctx.fakeUser.features = { aiUsageQuota: 'basic' } + const bundleFeatures = { aiErrorAssistant: true } + ctx.fakeUser.features = { aiErrorAssistant: false } ctx.Modules.promises.hooks.fire = sinon.stub().resolves([bundleFeatures]) const features = await ctx.UserGetter.promises.getUserFeatures( ctx.fakeUser._id diff --git a/services/web/types/user.ts b/services/web/types/user.ts index aae945d3c9..d039b95f83 100644 --- a/services/web/types/user.ts +++ b/services/web/types/user.ts @@ -10,7 +10,6 @@ export type UserId = Brand export type Features = { aiErrorAssistant?: boolean - aiUsageQuota?: string collaborators?: number compileGroup?: 'standard' | 'priority' compileTimeout?: number