Browse Source

Merge pull request #23972 from overleaf/jdt-grant-assist-via-wf-set-trait

enable granting of premium error assist based on WF entitlement to bu…

GitOrigin-RevId: 9d21cf8755c881bdc698c0cf9891076ecefd34eb
Jimmy Domagala-Tang 1 năm trước cách đây
mục cha
commit
80abd0ac2c

+ 16 - 1
services/web/app/src/Features/Project/ProjectController.js

@@ -15,6 +15,7 @@ const metrics = require('@overleaf/metrics')
 const { User } = require('../../models/User')
 const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
 const LimitationsManager = require('../Subscription/LimitationsManager')
+const FeaturesHelper = require('../Subscription/FeaturesHelper')
 const Settings = require('@overleaf/settings')
 const AuthorizationManager = require('../Authorization/AuthorizationManager')
 const InactiveProjectManager = require('../InactiveData/InactiveProjectManager')
@@ -760,6 +761,20 @@ const _ProjectController = {
       const isOverleafAssistBundleEnabled =
         splitTestAssignments['overleaf-assist-bundle']?.variant === 'enabled'
 
+      let fullFeatureSet = user?.features
+      if (!anonymous) {
+        // generate users feature set including features added, or overriden via modules
+        const moduleFeatures =
+          (await Modules.promises.hooks.fire(
+            'getModuleProvidedFeatures',
+            userId
+          )) || []
+        fullFeatureSet = FeaturesHelper.computeFeatureSet([
+          user.features,
+          ...moduleFeatures,
+        ])
+      }
+
       const isPaywallChangeCompileTimeoutEnabled =
         splitTestAssignments['paywall-change-compile-timeout']?.variant ===
         'enabled'
@@ -802,7 +817,7 @@ const _ProjectController = {
           allowedFreeTrial,
           hasRecurlySubscription: subscription?.recurlySubscription_id != null,
           featureSwitches: user.featureSwitches,
-          features: user.features,
+          features: fullFeatureSet,
           featureUsage,
           refProviders: _.mapValues(user.refProviders, Boolean),
           writefull: {

+ 8 - 0
services/web/app/src/Features/Subscription/FeaturesHelper.js

@@ -1,6 +1,13 @@
 const _ = require('lodash')
 const Settings = require('@overleaf/settings')
 
+/**
+ * merges an array of feature sets to produce a final feature set
+ */
+function computeFeatureSet(featureSets) {
+  return featureSets.reduce(mergeFeatures, {})
+}
+
 /**
  * Merge feature sets coming from different sources
  */
@@ -108,6 +115,7 @@ function getMatchedFeatureSet(features) {
 
 module.exports = {
   mergeFeatures,
+  computeFeatureSet,
   isFeatureSetBetter,
   compareFeatures,
   getMatchedFeatureSet,

+ 1 - 0
services/web/app/src/models/User.js

@@ -194,6 +194,7 @@ const UserSchema = new Schema(
     writefull: {
       enabled: { type: Boolean, default: null },
       autoCreatedAccount: { type: Boolean, default: false },
+      isPremium: { type: Boolean, default: false },
     },
     aiErrorAssistant: {
       enabled: { type: Boolean, default: true },

+ 38 - 0
services/web/test/unit/src/Project/ProjectControllerTests.js

@@ -1121,6 +1121,44 @@ describe('ProjectController', function () {
         this.ProjectController.loadEditor(this.req, this.res)
       })
     })
+
+    describe('when fetching the users featureSet', function () {
+      beforeEach(function () {
+        this.Modules.promises.hooks.fire = sinon.stub().resolves()
+        this.user.features = {}
+      })
+
+      it('should take into account features overrides from modules', function (done) {
+        // this case occurs when the user has bought the ai bundle on WF, which should include our error assistant
+        const bundleFeatures = { aiErrorAssistant: true }
+        this.user.features = { aiErrorAssistant: false }
+        this.Modules.promises.hooks.fire = sinon
+          .stub()
+          .resolves([bundleFeatures])
+        this.res.render = (pageName, opts) => {
+          expect(opts.user.features).to.deep.equal(bundleFeatures)
+          this.Modules.promises.hooks.fire.should.have.been.calledWith(
+            'getModuleProvidedFeatures',
+            this.user._id
+          )
+          done()
+        }
+        this.ProjectController.loadEditor(this.req, this.res)
+      })
+
+      it('should handle modules not returning any features', function (done) {
+        this.Modules.promises.hooks.fire = sinon.stub().resolves([])
+        this.res.render = (pageName, opts) => {
+          expect(opts.user.features).to.deep.equal({})
+          this.Modules.promises.hooks.fire.should.have.been.calledWith(
+            'getModuleProvidedFeatures',
+            this.user._id
+          )
+          done()
+        }
+        this.ProjectController.loadEditor(this.req, this.res)
+      })
+    })
   })
 
   describe('userProjectsJson', function () {

+ 28 - 0
services/web/test/unit/src/Subscription/FeaturesHelperTests.js

@@ -88,6 +88,34 @@ describe('FeaturesHelper', function () {
     })
   })
 
+  describe('computeFeatureSet', function () {
+    it('should handle only one featureSet', function () {
+      expect(
+        this.FeaturesHelper.computeFeatureSet([
+          { github: true, feat1: true, feat2: false },
+        ])
+      ).to.deep.equal({ github: true, feat1: true, feat2: false })
+    })
+    it('should handle an empty array of featureSets', function () {
+      expect(this.FeaturesHelper.computeFeatureSet([])).to.deep.equal({})
+    })
+
+    it('should handle 3+ featureSets', function () {
+      const featureSets = [
+        { github: true, feat1: false, feat2: false },
+        { github: false, feat1: true, feat2: false, feat3: false },
+        { github: false, feat1: false, feat2: true, feat4: true },
+      ]
+      expect(this.FeaturesHelper.computeFeatureSet(featureSets)).to.deep.equal({
+        github: true,
+        feat1: true,
+        feat2: true,
+        feat3: false,
+        feat4: true,
+      })
+    })
+  })
+
   describe('isFeatureSetBetter', function () {
     it('simple comparisons', function () {
       const result1 = this.FeaturesHelper.isFeatureSetBetter(