Skip to content

Commit

Permalink
V8: Email Marketing Opt In (umbraco#7366)
Browse files Browse the repository at this point in the history
* Enable BackOfficeTours to have a bool to hide them in the help drawer

* New hidden tour to display the email marketing option on login to backoffice

* Update to tourService to use the new bool property of hidden to show/hide the tour in the help drawer

* AngularJS Resource to call the Azure Function EmailService proxy code - currently set to DEV

* New method on userService.addUserToEmailMarketing that in turn calls the new emailMarketingResource

* New AngularJS view & controller in the tour step to deal with user clicking yes/accept to the email opt-in

* Modifies the init script to auto launch the hidden email marketing tour at login
If it has been accepted or dismissed before we then try to launch the original intro tour

* Only show the email marketing tour when the intro tour has been dismissed or completed and will appear for one time only the next time you login

* When using X to close email tour, it does not disable and never show it again but just closes it, similar to intro tour

* Adds new localStorageService key for 'emailMarketingTourShown' to prevent the tour being shown again in the same logged in session, if you refresh the backoffice in your browser

* Update URL to email function

* Adding new COMA copy for email marketing tour - needs fine tuning pixel pushing from Niels L

* Prettified layout of e-mail marketing promotion tour

* fixing whitespace

* text=auto

* adding xml to gitattributes

* Ensures the email tour is not shown if you dismiss the intro tour and manually refresh the page

Co-authored-by: Niels Lyngsø <[email protected]>
  • Loading branch information
2 people authored and elit0451 committed Jan 21, 2020
1 parent be38b96 commit 466f8ca
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 16 deletions.
16 changes: 10 additions & 6 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 9 additions & 3 deletions src/Umbraco.Web.UI.Client/src/common/services/tour.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@
group.groupOrder = item.groupOrder
}
groupExists = true;
group.tours.push(item)

if(item.hidden === false){
group.tours.push(item);
}
}
});

Expand All @@ -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);
}
}

});
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
};

Expand Down
30 changes: 27 additions & 3 deletions src/Umbraco.Web.UI.Client/src/init.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}
}
});
}
}
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/src/less/belle.less
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@

@import "components/contextdialogs/umb-dialog-datatype-delete.less";

@import "components/umbemailmarketing.less";


// Utilities
@import "utilities/layout/_display.less";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 6 additions & 1 deletion src/Umbraco.Web.UI.Client/src/main.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div ng-controller="Umbraco.Tours.UmbEmailMarketing.EmailsController as vm" class="umb-email-marketing">

<umb-tour-step on-close="model.endTour()">

<h2>{{ model.currentStep.title }}</h2>

<div class="layout">
<!-- HTML Content stored in tour JSON config file -->
<div class="primary">
<div ng-bind-html="model.currentStep.content"></div>
</div>

<!-- Secondary Column with paperplane -->
<div class="secondary">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 220 1080 640"><defs><style>.cls-1{fill:#fba0a2;}.cls-2{fill:#3544b1;}</style></defs><title>paperplane</title><g id="Layer_54" data-name="Layer 54"><polygon class="cls-1" points="1012.92 370.19 894.84 709.81 832.53 529.1 691.24 457.68 1012.92 370.19"/><polygon class="cls-2" points="1012.92 370.19 832.53 529.1 894.84 709.81 1012.92 370.19"/><path class="cls-2" d="M784.24,590.07s-34.16,66.82-88,70.95l-11.65-32s46.67-12.34,79.51-42.3Z"/><path class="cls-2" d="M633.49,670s-66.81,25.65-94.62,7l-3.5-26s35.92,19.8,79.74-9.67Z"/><path class="cls-2" d="M475.1,673.19S405.19,650.45,383,623.94l4.21-15.08,95.94,33.81Z"/><path class="cls-2" d="M311.51,601.88s-36.8-46.78-77.3-41.65l3.14-21.17s36.46-21.71,82.28,31.89Z"/><path class="cls-2" d="M183.21,562.23S119,565.32,89.29,588.69L67.08,569.22s61.6-41.75,103.69-36.05Z"/></g></svg>
</div>
</div>

<div class="cta">
<umb-button size="m" button-style="link" type="button" label="No thanks" action="model.disableTour()"></umb-button>
<umb-button size="m" button-style="action" type="button" action="vm.optIn()" label="Keep me updated!"></umb-button>
</div>

</umb-tour-step>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<div class="umb-tour__pulse"></div>

<div class="umb-tour__popover shadow-depth-2" ng-class="{'umb-tour__popover--l': model.currentStep.type === 'intro' || model.currentStepIndex === model.steps.length}">
<div class="umb-tour__popover shadow-depth-2" ng-class="{'umb-tour__popover--l': model.currentStep.type === 'intro' || model.currentStepIndex === model.steps.length, 'umb-tour__popover--promotion': model.currentStep.type === 'promotion'}">

<div ng-if="!configuredView && !elementNotFound">

Expand Down
7 changes: 6 additions & 1 deletion src/Umbraco.Web.UI/Umbraco/js/main.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json
Original file line number Diff line number Diff line change
@@ -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": "<p>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.</p> <p class='notice'>By signing up, you agree that we can use your info according to our <a href='https://umbraco.com/about-us/privacy/'>privacy policy</a>.</p>",
"view": "emails",
"type": "promotion"
}
]
},
{
"name": "Introduction",
"alias": "umbIntroIntroduction",
Expand Down
9 changes: 9 additions & 0 deletions src/Umbraco.Web/Models/BackOfficeTour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> RequiredSections { get; set; }

[DataMember(Name = "steps")]
public BackOfficeTourStep[] Steps { get; set; }

Expand Down

0 comments on commit 466f8ca

Please sign in to comment.