diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index dd4706884b..6f40606533 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -446,30 +446,6 @@ const _ProjectController = { usedLatex, } = userValues - // check if a user is not in the writefull-oauth-promotion, in which case they may be part of the auto trial group - if ( - !anonymous && - splitTestAssignments['writefull-oauth-promotion']?.variant === 'default' - ) { - // since we are auto-enrolling users into writefull if they are part of the group, we only want to - // auto enroll (set writefull to true) if its the first time they have entered the test - // this ensures that they can still turn writefull off (otherwise, we would be setting writefull on every time they access their projects) - const { variant, metadata } = - await SplitTestHandler.promises.getAssignment( - req, - res, - 'writefull-auto-load' - ) - if (variant === 'enabled' && metadata?.isFirstNonDefaultAssignment) { - await UserUpdater.promises.updateUser(userId, { - $set: { - writefull: { enabled: true }, - }, - }) - user.writefull.enabled = true - } - } - const brandVariation = project?.brandVariationId ? await BrandVariationsHandler.promises.getBrandVariationById( project.brandVariationId @@ -636,16 +612,15 @@ const _ProjectController = { !userIsMemberOfGroupSubscription && !userHasInstitutionLicence - let showAiErrorAssistant = false + let aiFeaturesAllowed = false if (userId && Features.hasFeature('saas')) { try { // exit early if the user couldnt use ai anyways, since permissions checks are expensive - const canUseAiOnProject = - user.features?.aiErrorAssistant && - (privilegeLevel === PrivilegeLevels.READ_AND_WRITE || - privilegeLevel === PrivilegeLevels.OWNER) + const canEditProject = + privilegeLevel === PrivilegeLevels.READ_AND_WRITE || + privilegeLevel === PrivilegeLevels.OWNER - if (canUseAiOnProject) { + if (canEditProject) { // check permissions for user and project owner, to see if they allow AI on the project const permissionsResults = await Modules.promises.hooks.fire( 'projectAllowsCapability', @@ -657,11 +632,34 @@ const _ProjectController = { result => result === true ) - showAiErrorAssistant = aiAllowed + aiFeaturesAllowed = aiAllowed } } catch (err) { // still allow users to access project if we cant get their permissions, but disable AI feature - showAiErrorAssistant = false + aiFeaturesAllowed = false + } + } + + // check if a user has never tried writefull before (writefull.enabled will be null) + // if they previously accepted writefull. user.writefull will be true, + // if they explicitly disabled it, user.writefull will be false + if (aiFeaturesAllowed && user.writefull?.enabled === null) { + // since we are auto-enrolling users into writefull if they are part of the group, we only want to + // auto enroll (set writefull to true) if its the first time they have entered the test + // this ensures that they can still turn writefull off (otherwise, we would be setting writefull on every time they access their projects) + const { variant, metadata } = + await SplitTestHandler.promises.getAssignment( + req, + res, + 'writefull-auto-load' + ) + if (variant === 'enabled' && metadata?.isFirstNonDefaultAssignment) { + await UserUpdater.promises.updateUser(userId, { + $set: { + writefull: { enabled: true }, + }, + }) + user.writefull.enabled = true } } @@ -693,7 +691,7 @@ const _ProjectController = { features: user.features, refProviders: _.mapValues(user.refProviders, Boolean), writefull: { - enabled: Boolean(user.writefull?.enabled), + enabled: Boolean(user.writefull?.enabled && aiFeaturesAllowed), }, alphaProgram: user.alphaProgram, betaProgram: user.betaProgram, @@ -741,7 +739,9 @@ const _ProjectController = { debugPdfDetach, showSymbolPalette, symbolPaletteAvailable: Features.hasFeature('symbol-palette'), - showAiErrorAssistant, + userRestrictions: Array.from(req.userRestrictions || []), + showAiErrorAssistant: + aiFeaturesAllowed && user.features?.aiErrorAssistant, detachRole, metadata: { viewport: false }, showUpgradePrompt, diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index 5552dad769..0f7564a166 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -71,13 +71,6 @@ async function settingsPage(req, res) { ) } - // getAssignment sets res.locals, which will pass to the splitTest context - await SplitTestHandler.promises.getAssignment( - req, - res, - 'writefull-oauth-promotion' - ) - let personalAccessTokens try { const results = await Modules.promises.hooks.fire( diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 01babf4cd5..41e1f7fd42 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -180,7 +180,7 @@ const UserSchema = new Schema( zotero: Schema.Types.Mixed, }, writefull: { - enabled: { type: Boolean, default: false }, + enabled: { type: Boolean, default: null }, }, alphaProgram: { type: Boolean, default: false }, // experimental features betaProgram: { type: Boolean, default: false }, diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx index 5a01806177..c899dfa503 100644 --- a/services/web/frontend/js/features/settings/components/linking-section.tsx +++ b/services/web/frontend/js/features/settings/components/linking-section.tsx @@ -5,7 +5,6 @@ import { useSSOContext, SSOSubscription } from '../context/sso-context' import { SSOLinkingWidget } from './linking/sso-widget' import getMeta from '../../../utils/meta' import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user' -import { useFeatureFlag } from '@/shared/context/split-test-context' import OLNotification from '@/features/ui/components/ol/ol-notification' const availableIntegrationLinkingWidgets = importOverleafModules( @@ -48,19 +47,7 @@ function LinkingSection() { oauth2ServerComponents ) - // currently the only thing that is in the langFeedback section is writefull, - // which is behind a split test. we should hide this section if the user is not in the split test - // todo: remove split test check, and split test context after gradual rollout is complete - const hasWritefullOauthPromotion = useFeatureFlag('writefull-oauth-promotion') - - // even if they arent in the split test, if they have it enabled let them toggle it off - const user = getMeta('ol-user') - const shouldLoadWritefull = - (hasWritefullOauthPromotion || user.writefull?.enabled === true) && - !window.writefull // check if the writefull extension is installed, in which case we dont handle the integration - - const haslangFeedbackLinkingWidgets = - langFeedbackLinkingWidgets.length && shouldLoadWritefull + const haslangFeedbackLinkingWidgets = langFeedbackLinkingWidgets.length const hasIntegrationLinkingSection = renderSyncSection && allIntegrationLinkingWidgets.length const hasReferencesLinkingSection = referenceLinkingWidgets.length diff --git a/services/web/scripts/split_writefull_disabled_from_unset.js b/services/web/scripts/split_writefull_disabled_from_unset.js new file mode 100644 index 0000000000..62708d7841 --- /dev/null +++ b/services/web/scripts/split_writefull_disabled_from_unset.js @@ -0,0 +1,59 @@ +const { db, waitForDb } = require('../app/src/infrastructure/mongodb') +const { batchedUpdate } = require('./helpers/batchedUpdate') +const { ObjectId } = require('mongodb-legacy') +const fs = require('fs') + +const CHUNK_SIZE = 1000 + +// Function to chunk the array +function chunkArray(array, size) { + const result = [] + for (let i = 0; i < array.length; i += size) { + result.push(array.slice(i, i + size)) + } + return result +} + +async function main() { + // search for file of users who already explicitly opted out first + const optOutPath = process.argv[2] + const optedOutFile = fs.readFileSync(optOutPath, 'utf8') + let optedOutList = optedOutFile + + optedOutList = optedOutFile.split('\n').map(id => new ObjectId(id)) + + console.log(`preserving opt-outs of ${optedOutList.length} users`) + await waitForDb() + // update all applicable user models + await batchedUpdate( + 'users', + { 'writefull.enabled': false }, // and is false + { $set: { 'writefull.enabled': null } } + ) + + const chunks = chunkArray(optedOutList, CHUNK_SIZE) + + // then reset any explicit false back to being false + // Iterate over each chunk and perform the query + for (const chunkedIds of chunks) { + console.log('batch update started') + await db.users.updateMany( + { _id: { $in: chunkedIds } }, + { $set: { 'writefull.enabled': false } } + ) + console.log('batch completed') + } +} + +module.exports = main + +if (require.main === module) { + main() + .then(() => { + process.exit(0) + }) + .catch(error => { + console.error({ error }) + process.exit(1) + }) +}