Browse Source

Show an "Upgrade plan" button to users that have reached maximum projects

Instead of letting users go through the project creation process only to get rejected with a "subscription limits reached" message, now the "New Library project" button is being replaced with an "Upgrade plan" button when the maximum allowed projects have been reached for the specific amount. The button is accompanied by a tooltip that explains the situation to the user. Once clicked, the user is redirected to the subscriptions page.

CURA-8112
Konstantinos Karmas 3 years ago
parent
commit
2d45b8c2cd

+ 16 - 1
plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml

@@ -18,7 +18,7 @@ Item
 
 
     width: parent.width
     width: parent.width
     height: parent.height
     height: parent.height
-    property alias createNewProjectButtonVisible: createNewProjectButton.visible
+    property bool createNewProjectButtonVisible: true
 
 
     anchors
     anchors
     {
     {
@@ -48,6 +48,7 @@ Item
         anchors.verticalCenter: selectProjectLabel.verticalCenter
         anchors.verticalCenter: selectProjectLabel.verticalCenter
         anchors.right: parent.right
         anchors.right: parent.right
         text: "New Library project"
         text: "New Library project"
+        visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
 
 
         onClicked:
         onClicked:
         {
         {
@@ -56,6 +57,20 @@ Item
         busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
         busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
     }
     }
 
 
+
+    Cura.SecondaryButton
+    {
+        id: upgradePlanButton
+
+        anchors.verticalCenter: selectProjectLabel.verticalCenter
+        anchors.right: parent.right
+        text: "Upgrade plan"
+        visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
+        tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects."
+
+        onClicked: Qt.openUrlExternally("https://ultimaker.com/software/enterprise-software")
+    }
+
     Item
     Item
     {
     {
         id: noLibraryProjectsContainer
         id: noLibraryProjectsContainer

+ 23 - 0
plugins/DigitalLibrary/src/DigitalFactoryApiClient.py

@@ -55,6 +55,7 @@ class DigitalFactoryApiClient:
         self._http = HttpRequestManager.getInstance()
         self._http = HttpRequestManager.getInstance()
         self._on_error = on_error
         self._on_error = on_error
         self._file_uploader = None  # type: Optional[DFFileUploader]
         self._file_uploader = None  # type: Optional[DFFileUploader]
+        self._library_max_private_projects: Optional[int] = None
 
 
         self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None  # type: Optional[PaginationManager]
         self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None  # type: Optional[PaginationManager]
 
 
@@ -69,6 +70,7 @@ class DigitalFactoryApiClient:
                 callback(
                 callback(
                     response.library_max_private_projects == -1 or  # Note: -1 is unlimited
                     response.library_max_private_projects == -1 or  # Note: -1 is unlimited
                     response.library_max_private_projects > 0)
                     response.library_max_private_projects > 0)
+                self._library_max_private_projects = response.library_max_private_projects
             else:
             else:
                 Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
                 Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
                 callback(False)
                 callback(False)
@@ -79,6 +81,27 @@ class DigitalFactoryApiClient:
                        error_callback = callbackWrap,
                        error_callback = callbackWrap,
                        timeout = self.DEFAULT_REQUEST_TIMEOUT)
                        timeout = self.DEFAULT_REQUEST_TIMEOUT)
 
 
+    def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None:
+        """
+        Checks if the user is allowed to create new library projects.
+        A user is allowed to create new library projects if the haven't reached their maximum allowed private projects.
+        """
+
+        def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
+            if response is not None:
+                if self._library_max_private_projects == -1 or isinstance(response, DigitalFactoryProjectResponse):
+                    callback(True)
+                elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response):
+                    callback(len(response) < self._library_max_private_projects)
+                else:
+                    Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}")
+                    callback(False)
+            else:
+                Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}")
+                callback(False)
+
+        self.getProjectsFirstPage(on_finished = callbackWrap, failed = callbackWrap)
+
     def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
     def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
         """
         """
         Retrieves a digital factory project by its library project id.
         Retrieves a digital factory project by its library project id.

+ 18 - 0
plugins/DigitalLibrary/src/DigitalFactoryController.py

@@ -92,6 +92,9 @@ class DigitalFactoryController(QObject):
     """Signal to inform about the state of user access."""
     """Signal to inform about the state of user access."""
     userAccessStateChanged = pyqtSignal(bool)
     userAccessStateChanged = pyqtSignal(bool)
 
 
+    """Signal to inform whether the user is allowed to create more Library projects."""
+    userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
+
     def __init__(self, application: CuraApplication) -> None:
     def __init__(self, application: CuraApplication) -> None:
         super().__init__(parent = None)
         super().__init__(parent = None)
 
 
@@ -136,6 +139,7 @@ class DigitalFactoryController(QObject):
         self._application.initializationFinished.connect(self._applicationInitializationFinished)
         self._application.initializationFinished.connect(self._applicationInitializationFinished)
 
 
         self._user_has_access = False
         self._user_has_access = False
+        self._user_account_can_create_new_project = False
 
 
     def clear(self) -> None:
     def clear(self) -> None:
         self._project_model.clearProjects()
         self._project_model.clearProjects()
@@ -166,6 +170,11 @@ class DigitalFactoryController(QObject):
             subscriptions = self._account.userProfile.get("subscriptions", [])
             subscriptions = self._account.userProfile.get("subscriptions", [])
             if len(subscriptions) > 0:
             if len(subscriptions) > 0:
                 return True
                 return True
+        if self._user_has_access:
+            # The user has access even though they have no subscriptions. This means they are an Essential user and they
+            # have limited personal private projects available. In this case, we need to check whether they have already
+            # reached their limit.
+            self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
         return self._user_has_access
         return self._user_has_access
 
 
     def initialize(self, preselected_project_id: Optional[str] = None) -> None:
     def initialize(self, preselected_project_id: Optional[str] = None) -> None:
@@ -517,6 +526,7 @@ class DigitalFactoryController(QObject):
             self._project_model.clearProjects()
             self._project_model.clearProjects()
             self.setSelectedProjectIndex(-1)
             self.setSelectedProjectIndex(-1)
             self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
             self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
+            self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
             self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
             self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
         self._has_preselected_project = new_has_preselected_project
         self._has_preselected_project = new_has_preselected_project
         self.preselectedProjectChanged.emit()
         self.preselectedProjectChanged.emit()
@@ -525,6 +535,14 @@ class DigitalFactoryController(QObject):
     def hasPreselectedProject(self) -> bool:
     def hasPreselectedProject(self) -> bool:
         return self._has_preselected_project
         return self._has_preselected_project
 
 
+    def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None:
+        self._user_account_can_create_new_project = can_create_new_library_project
+        self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project)
+
+    @pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged)
+    def userAccountCanCreateNewLibraryProject(self) -> bool:
+        return self._user_account_can_create_new_project
+
     @pyqtSlot(str, "QStringList")
     @pyqtSlot(str, "QStringList")
     def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
     def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
         """
         """