In my previous blog post Part 2 - Custom Field for Marketing Preferences and ContactList we were able to get a manager root and a contact list.

By the way, there is a config and a setting if you want to use the double-opt-in option! Not all of you have to bear the european GDPR in mind.

Okay, I'm now able to choose a manager root and a contact list, both will be available in the form field model.

Let's do the custom submit action, which saves the preferences to the contact! From now on I will use all the services which Sitecore used internally!

There is a class which provides the Contact Identifier and is used for the submit action, but the whole magic happens in the base class of the submit action:

using System;
using System.Collections.Generic;
using System.Linq;
using Feature.SitecoreForms.MarketingCategoriesSubscription.Exm.Managers;
using Feature.SitecoreForms.MarketingCategoriesSubscription.Exm.Services.Contact;
using Feature.SitecoreForms.MarketingCategoriesSubscription.Exm.Services.MarketingPreferences;
using Feature.SitecoreForms.MarketingCategoriesSubscription.Forms.FieldTypes;
using Feature.SitecoreForms.MarketingCategoriesSubscription.Forms.SubmitActions.Data;
using Feature.SitecoreForms.MarketingCategoriesSubscription.XConnect.Factories;
using Feature.SitecoreForms.MarketingCategoriesSubscription.XConnect.Services;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.EmailCampaign.Cd.Services;
using Sitecore.EmailCampaign.Model.XConnect;
using Sitecore.EmailCampaign.Model.XConnect.Facets;
using Sitecore.ExM.Framework.Diagnostics;
using Sitecore.ExperienceForms.Models;
using Sitecore.ExperienceForms.Processing;
using Sitecore.ExperienceForms.Processing.Actions;
using Sitecore.Framework.Conditions;
using Sitecore.ListManagement;
using Sitecore.Marketing.Definitions.ContactLists;
using Sitecore.Modules.EmailCampaign;
using Sitecore.Modules.EmailCampaign.ListManager;
using Sitecore.Modules.EmailCampaign.Services;
using Sitecore.XConnect;
using Sitecore.XConnect.Collection.Model;

namespace Feature.SitecoreForms.MarketingCategoriesSubscription.Forms.SubmitActions.Base
{
    public abstract class MarketingPreferencesBase<T> : SubmitActionBase<T> where T : MarketingPreferencesData
    {
        private readonly IXConnectService _xConnectService;
        private readonly IExmContactService _exmContactService;
        private readonly IExmSubscriptionManager _exmSubscriptionManager;
        private readonly ICustomMarketingPreferencesService _marketingPreferenceService;
        private readonly IClientApiService _clientApiService;
        private readonly IManagerRootService _managerRootService;
        private readonly ListManagerWrapper _listManagerWrapper;
        private readonly bool _useDoubleOptIn = Settings.GetBoolSetting("NewsletterSubscription.UseDoubleOptInForSubscription", true); // GDPR, sorry for the default value!

        protected new readonly ILogger Logger;
        private readonly IXConnectContactFactory _xConnectContactFactory;

        protected MarketingPreferencesBase(
            ISubmitActionData submitActionData,
            ILogger logger,
            IXConnectContactFactory xConnectContactFactory,
            IXConnectService xConnectService,
            IExmContactService exmContactService,
            IExmSubscriptionManager exmSubscriptionManager,
            ICustomMarketingPreferencesService marketingPreferenceService,
            IClientApiService clientApiService,
            IManagerRootService managerRootService,
            ListManagerWrapper listManagerWrapper) : base(submitActionData)
        {
            Condition.Requires(logger, nameof(logger)).IsNotNull();
            Condition.Requires(xConnectContactFactory, nameof(xConnectContactFactory)).IsNotNull();
            Condition.Requires(xConnectService, nameof(xConnectService)).IsNotNull();
            Condition.Requires(exmContactService, nameof(exmContactService)).IsNotNull();
            Condition.Requires(exmSubscriptionManager, nameof(exmSubscriptionManager)).IsNotNull();
            Condition.Requires(marketingPreferenceService, nameof(marketingPreferenceService)).IsNotNull();
            Condition.Requires(clientApiService, nameof(clientApiService)).IsNotNull();
            Condition.Requires(managerRootService, nameof(managerRootService)).IsNotNull();
            Condition.Requires(listManagerWrapper, nameof(listManagerWrapper)).IsNotNull();

            Logger = logger;
            _xConnectContactFactory = xConnectContactFactory;
            _xConnectService = xConnectService;
            _exmContactService = exmContactService;
            _exmSubscriptionManager = exmSubscriptionManager;
            _marketingPreferenceService = marketingPreferenceService;
            _clientApiService = clientApiService;
            _managerRootService = managerRootService;
            _listManagerWrapper = listManagerWrapper;
        }

        protected override bool Execute(T data, FormSubmitContext formSubmitContext)
        {
            Assert.ArgumentNotNull(formSubmitContext, nameof(formSubmitContext));

            if (data.FieldEmailAddressId == Guid.Empty)
            {
                Logger.LogWarn("Field email address couldn't be empty!");
                return false;
            }

            try
            {
                if (!formSubmitContext.HasErrors)
                {
                    ProcessContact(data, formSubmitContext.Fields);
                }
                else
                {
                    Logger.LogWarn($"Form {formSubmitContext.FormId} submitted with errors: {string.Join(", ", formSubmitContext.Errors.Select(t => t.ErrorMessage))}.");
                }

                return true;
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.Message, ex);
                return false;
            }
        }

        private void ProcessContact(T data, IList<IViewModel> fields)
        {
            var contactIdentifier = GetContactIdentifier(data, fields);
            if (contactIdentifier == null)
            {
                Logger.LogWarn("A contact must have a valid identifiersource and identifiervalue!");
                return;
            }

            var customXConnectContact = _xConnectContactFactory.CreateContactWithEmail(contactIdentifier.Identifier);

            _xConnectService.UpdateOrCreateContact(customXConnectContact);

            var contact = _xConnectService.GetXConnectContact(contactIdentifier, ExmKeyBehaviorCache.DefaultFacetKey, EmailAddressList.DefaultFacetKey, ListSubscriptions.DefaultFacetKey);
            if (contact == null)
            {
                Logger.LogWarn("Contact not found!");
                return;
            }

            _exmContactService.EnsureExmKeyBehaviorCache(contact);

            var newsletterSubscriptionViewModel = GetNewsletterSubscriptionViewModel(fields);
            if (newsletterSubscriptionViewModel == null)
            {
                Logger.LogError("Newsletter Subscription field is null!");
                return;
            }

            var managerRoot = GetManagerRoot(newsletterSubscriptionViewModel);
            if (managerRoot == null)
            {
                Logger.LogError("ManagerRoot is null! In order to subscribe a contact to marketing categories you have to select a valid manager root!");
                return;
            }

            var contactList = GetContactList(newsletterSubscriptionViewModel);
            if (contactList == null)
            {
                Logger.LogError("ContactList is null!");
            }

            var marketingPreferences = GetSelectedMarketingPreferences(newsletterSubscriptionViewModel, managerRoot, contact.ExmKeyBehaviorCache()?.MarketingPreferences).ToList();
            var unsubscribeFromAll = marketingPreferences.All(x => x.Preference == false);
            if (unsubscribeFromAll)
            {
                _marketingPreferenceService.SavePreferences(contact, marketingPreferences);
                _exmSubscriptionManager.UnsubscribeFromAll(contact, managerRoot);
            }
            else
            {
                SubscribeContact(managerRoot, contactList, contact, marketingPreferences);
            }
        }

        private ManagerRoot GetManagerRoot(MarketingPreferencesViewModel model)
        {
            return model == null || string.IsNullOrEmpty(model.ManagerRootId) ? null : _managerRootService.GetManagerRoot(new Guid(model.ManagerRootId));
        }

        private ContactList GetContactList(MarketingPreferencesViewModel model)
        {
            return model == null || string.IsNullOrEmpty(model.ContactListId) ? null : _listManagerWrapper.FindById(new Guid(model.ContactListId));
        }

        private static IEnumerable<MarketingPreference> GetSelectedMarketingPreferences(MarketingPreferencesViewModel model, ManagerRoot managerRoot, IReadOnlyCollection<MarketingPreference> contactMarketingPreferences)
        {
            var preferencesList = new List<MarketingPreference>();
            if (model == null || !model.Value.Any())
            {
                return preferencesList;
            }

            foreach (var listFieldItem in model.Items)
            {
                if (contactMarketingPreferences != null && contactMarketingPreferences.Any(x => x.MarketingCategoryId.ToString("B").ToUpper() == listFieldItem.Value))
                {
                    preferencesList.Add(CreateMarketingPreference(managerRoot, listFieldItem.Value, listFieldItem.Selected));
                }
                else if (model.Value.Contains(listFieldItem.Value))
                {
                    preferencesList.Add(CreateMarketingPreference(managerRoot, listFieldItem.Value, true));
                }
            }

            return preferencesList;
        }

        private static MarketingPreference CreateMarketingPreference(ManagerRoot managerRoot, string preferenceId, bool selectedPreference)
        {
            var database = Context.ContentDatabase ?? Context.Database;
            var marketingPreference = new MarketingPreference
            {
                DateTime = DateTime.UtcNow,
                ManagerRootId = managerRoot.Id,
                MarketingCategoryId = database.GetItem(new ID(preferenceId)).ID.ToGuid(),
                Preference = selectedPreference
            };
            return marketingPreference;
        }

        private void SubscribeContact(ManagerRoot managerRoot, ContactList contactList, Contact contact, List<MarketingPreference> marketingPreferences)
        {
            if (_useDoubleOptIn)
            {
                if (managerRoot == null)
                {
                    throw new ArgumentNullException(nameof(managerRoot), "To subscribe a contact to a contact list, a valid manager root is needed!");
                }

                if (contactList.ContactListDefinition.Id == Guid.Empty || contactList.ContactListDefinition.Type == ListType.SegmentedList)
                {
                    throw new ArgumentNullException(nameof(contactList), "To subscribe a contact to a contact list, a valid contact list is needed and the list should not be segmented!");
                }

                _exmSubscriptionManager.Subscribe(contact, contactList.ContactListDefinition.Id, managerRoot, true);
            }

            _marketingPreferenceService.SavePreferences(contact, marketingPreferences);
        }

        private static MarketingPreferencesViewModel GetNewsletterSubscriptionViewModel(IEnumerable<IViewModel> fields)
        {
            var newsletterSubscriptionField = fields.Where(field => field.GetType() == typeof(MarketingPreferencesViewModel)).Cast<MarketingPreferencesViewModel>().FirstOrDefault();
            return newsletterSubscriptionField;
        }

        protected abstract ContactIdentifier GetContactIdentifier(T data, IEnumerable<IViewModel> fields);
    }
}

There is much logic inside, but most of it is to ensure everything is in place!

It is important to ensure that the ExmBehaviorCache and the MarketingPreferences are initialized! Especially for new contacts!

The most important things I had to take care of, were to check that everything is in place, then check the double-opt-in setting and subscribe the contact to the global contact list using the Sitecore "SubscriptionManager" and its double-opt-in feature.

It is also important to save the preferences with the Sitecore "MarketingPreferenceService"! If you do not care about the GDPR you will always be subscribed to the preferences you selected. ;)

Now, go on to the next post in this series Part 4 - Custom field to identify contacts.

Happy submitting
Dirk

The author

dirk-autor
Dirk Schäfauer
Dirk bei LinkedIn