diff --git a/.gitattributes b/.gitattributes index a664be3a85..c8987ade67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,7 @@ *.png binary *.gif binary -*.cs text=auto diff=csharp +*.cs text=auto diff=csharp *.vb text=auto *.c text=auto *.cpp text=auto @@ -41,9 +41,13 @@ *.fs text=auto *.fsx text=auto *.hs text=auto +*.json text=auto +*.xml text=auto -*.csproj text=auto merge=union -*.vbproj text=auto merge=union -*.fsproj text=auto merge=union -*.dbproj text=auto merge=union -*.sln text=auto eol=crlf merge=union +*.csproj text=auto merge=union +*.vbproj text=auto merge=union +*.fsproj text=auto merge=union +*.dbproj text=auto merge=union +*.sln text=auto eol=crlf merge=union + +*.gitattributes text=auto diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js new file mode 100644 index 0000000000..4ac56ad13b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js @@ -0,0 +1,34 @@ +/** + * @ngdoc service + * @name umbraco.resources.emailMarketingResource + * @description Used to add a backoffice user to Umbraco's email marketing system, if user opts in + * + * + **/ +function emailMarketingResource($http, umbRequestHelper) { + + // LOCAL + // http://localhost:7071/api/EmailProxy + + // LIVE + // https://emailcollector.umbraco.io/api/EmailProxy + + const emailApiUrl = 'https://emailcollector.umbraco.io/api/EmailProxy'; + + //the factory object returned + return { + + postAddUserToEmailMarketing: (user) => { + return umbRequestHelper.resourcePromise( + $http.post(emailApiUrl, + { + name: user.name, + email: user.email, + usergroup: user.userGroups // [ "admin", "sensitiveData" ] + }), + 'Failed to add user to email marketing list'); + } + }; +} + +angular.module('umbraco.resources').factory('emailMarketingResource', emailMarketingResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index e102da5d34..62af17146c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -147,7 +147,10 @@ group.groupOrder = item.groupOrder } groupExists = true; - group.tours.push(item) + + if(item.hidden === false){ + group.tours.push(item); + } } }); @@ -157,8 +160,11 @@ if(item.groupOrder) { newGroup.groupOrder = item.groupOrder } - newGroup.tours.push(item); - groupedTours.push(newGroup); + + if(item.hidden === false){ + newGroup.tours.push(item); + groupedTours.push(newGroup); + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 7723c8f4bb..afd7b606e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, $timeout, angularHelper) { + .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; @@ -262,6 +262,11 @@ angular.module('umbraco.services') /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ setUserTimeout: function (newTimeout) { setUserTimeoutInternal(newTimeout); + }, + + /** Calls out to a Remote Azure Function to deal with email marketing service */ + addUserToEmailMarketing: (user) => { + return emailMarketingResource.postAddUserToEmailMarketing(user); } }; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 7d199c5c4f..d5c5166d21 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', - function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) { +app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', 'localStorageService', + function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService, localStorageService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -23,11 +23,35 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', appReady(data); tourService.registerAllTours().then(function () { - // Auto start intro tour + + // Start intro tour tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) { // start intro tour if it hasn't been completed or disabled if (introTour && introTour.disabled !== true && introTour.completed !== true) { tourService.startTour(introTour); + localStorageService.set("introTourShown", true); + } + else { + + const introTourShown = localStorageService.get("introTourShown"); + if(!introTourShown){ + // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) + tourService.getTourByAlias("umbEmailMarketing").then(function (emailMarketingTour) { + // Only show the email marketing tour one time - dismissing it or saying no will make sure it never appears again + // Unless invoked from tourService JS Client code explicitly. + // Accepted mails = Completed and Declicned mails = Disabled + if (emailMarketingTour && emailMarketingTour.disabled !== true && emailMarketingTour.completed !== true) { + + // Only show the email tour once per logged in session + // The localstorage key is removed on logout or user session timeout + const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); + if(!emailMarketingTourShown){ + tourService.startTour(emailMarketingTour); + localStorageService.set("emailMarketingTourShown", true); + } + } + }); + } } }); }); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b5e032f9fb..0921f46aac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -194,6 +194,8 @@ @import "components/contextdialogs/umb-dialog-datatype-delete.less"; +@import "components/umbemailmarketing.less"; + // Utilities @import "utilities/layout/_display.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 42403c65b1..bf2f030cea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -119,3 +119,17 @@ border: none; padding: 0; } + +.umb-tour__popover--promotion { + width: 800px; + min-height: 400px; + padding: 40px; + border-radius: @baseBorderRadius * 2; + .umb-tour-step__close { + top: 40px; + right: 40px; + } + a { + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less new file mode 100644 index 0000000000..f4b3183045 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less @@ -0,0 +1,44 @@ +.umb-email-marketing { + + h2 { + font-weight: 800; + max-width: 26ex; + margin-top: 20px; + } + + .layout { + display: flex; + align-items: center; + align-content: stretch; + + .primary { + flex-basis: 50%; + padding-right: 40px; + padding-top: 20px; + padding-bottom: 20px; + .notice { + color: @gray-5; + font-style: italic; + a { + color: @gray-5; + &:hover { + color: @ui-action-type-hover; + } + } + } + } + + .secondary { + flex-basis: 50%; + svg { + height: 200px; + width: 100%; + margin-top: -60px; + } + } + } + + .cta { + text-align: right; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js new file mode 100644 index 0000000000..8ecc737278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function EmailsController($scope, userService) { + + var vm = this; + + vm.optIn = function() { + // Get the current user in backoffice + userService.getCurrentUser().then(function(user){ + // Send this user along to opt in + // It's a fire & forget - not sure we need to check the response + userService.addUserToEmailMarketing(user); + }); + + // Mark Tour as complete + // This is also can help us indicate that the user accepted + // Where disabled is set if user closes modal or chooses NO + $scope.model.completeTour(); + } + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbEmailMarketing.EmailsController", EmailsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html new file mode 100644 index 0000000000..887624ed05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html @@ -0,0 +1,26 @@ +
+ + + +

{{ model.currentStep.title }}

+ +
+ +
+
+
+ + +
+ paperplane +
+
+ +
+ + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index 5dd56941a9..e358d75b9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -4,7 +4,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index e300e6562e..7b3f2a2184 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -1,4 +1,22 @@ [ + { + "name": "Email Marketing", + "alias": "umbEmailMarketing", + "group": "Email Marketing", + "groupOrder": 10, + "hidden": true, + "requiredSections": [ + "content" + ], + "steps": [ + { + "title": "Do you want to stay updated on everything Umbraco?", + "content": "

Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

By signing up, you agree that we can use your info according to our privacy policy.

", + "view": "emails", + "type": "promotion" + } + ] + }, { "name": "Introduction", "alias": "umbIntroIntroduction", diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..7391765193 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -16,16 +16,25 @@ namespace Umbraco.Web.Models [DataMember(Name = "name")] public string Name { get; set; } + [DataMember(Name = "alias")] public string Alias { get; set; } + [DataMember(Name = "group")] public string Group { get; set; } + [DataMember(Name = "groupOrder")] public int GroupOrder { get; set; } + + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } + [DataMember(Name = "allowDisable")] public bool AllowDisable { get; set; } + [DataMember(Name = "requiredSections")] public List RequiredSections { get; set; } + [DataMember(Name = "steps")] public BackOfficeTourStep[] Steps { get; set; }