diff --git a/.gitattributes b/.gitattributes index a664be3a859b..c8987ade67b6 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 000000000000..4ac56ad13b01 --- /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 e102da5d34f9..62af17146c70 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 7723c8f4bbb0..afd7b606e788 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 7d199c5c4fc0..d5c5166d214d 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 b5e032f9fbe4..0921f46aac13 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 42403c65b167..bf2f030ceaf1 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 000000000000..f4b318304559 --- /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 93870f8a5626..883907d1dc8d 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 000000000000..8ecc73727864 --- /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 000000000000..887624ed0585 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html @@ -0,0 +1,26 @@ +
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 d5987ec5bc4e..739176519301 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -16,16 +16,25 @@ public BackOfficeTour() [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