Sana Assistant (online)
Table of Contents

Implementing New Product Configurator

From this article you will learn how to create custom product configurator extension.

Please use the following reference articles to find more details of extensions infrastructure:

About product configurators

Starting with version 1.3.0 of the extension framework Sana supports external product configurators via new extension point. Custom product configurators can be built upon this extension point. Product configurator is an external application with which Sana communicates under the hood while a user is configuring a product.

User experience

For the web store user the product configurator experience looks like the following.

Certain products in the web store may be marked by ERP administrator as configurable. In this case, for such products, on product details page in the web store the user sees a "Configure" button instead of "Add to cart" button. Clicking "Configure" button the user opens a popup on the page where Sana renders an iframe which contains external product configurator page. This is an important point: Sana does not provide product configurator functionality, it only uses external configurator application for this. So the external product configurator page is shown in the popup and the user configures the product according to their choice, and after they finish configuring the product they click "Save" button from within the product configurator. The popup then is closed and the configured product is added to the shopping cart.

"Configure" button on product details page:

Configure Button on product details page

Sample configurator application opens in an iframe in a popup after "Configure" button is clicked. The content is not rendered by Sana, it is rendered by an external web application, in our case we created a simple configurator application for this tutorial. Sana only provides an iframe and relies on the external configurator application to provide the actual user experience of configuring the product:

Sample Configurator Popup

After the user is done configuring the product in the popup, they add the product to the basket from within the popup. The external configurator application should provide some button for this:

Configured Product Added To Basket

Under the hood

Now let's get a quick overview of what happens under the hood. As it has been mentioned earlier, Sana does not provide the actual configurator implementation, instead it relies on external application to do the actual configuration and return configured product to Sana. This interaction between Sana and the external configurator application is done through an extension add-on, which this article is all about.

Sequence Diagram

So there are three main parties: Sana, the extension add-on and the external configurator web application.

  1. First step is for Sana to detect whether a product is configurable or not. For that Sana performs a check for every product, when they are displayed. Sana looks through all active product configurator extension add-ons, currently installed in the system, and for each of them Sana asks whether it can recognize this product as configurable. The add-on must implement DetectConfigurableProductAsync method for this.

  2. When Sana gets positive response from one of product configurator extension add-ons, it renders "Configure" button instead of usual "Add to cart" button.

  3. When the user clicks "Configure" button, a popup is shown and Sana renders an iframe where it will show the page of the external configurator web application. For this Sana needs to know the URL of that page. For this Sana calls GetConfiguratorUrlAsync method of the extension add-on. The add-on must build the URL including all parameters that are needed to be passed to the external configurator application. Sana sets this URL in iframe's src attribute and thus the user sees the external web application in the popup, where they can configure the product to their liking.

  4. Whenever the user is done configuring the product, Sana expects that the external configurator application will send the result using one of the two possible ways.

  • The first is to send data as a message from iframe via the browser window. The message format can be anything. Example:
    var message = '[configuration data]';
    parent.postMessage(message, '*');
  • The second is to send post request to the callback URL (see the CallbackUrl property in the ProductConfiguratorContext). In this case you still need to post an empty message via the browser window in order to close the popup with the iframe in Sana, because it does not close itself automatically. Example:
    // We're using JQuery in our example here:
    $.ajax({
      type: 'POST',
      // The `callbackUrl` is passed to the external configurator application with the other parameters during URL building.
      url: callbackUrl,
      crossDomain: true,
      data: '[configuration data]',
      // We still need to post an empty message via the browser window in order to close the popup automatically:
      success: function () {
        parent.postMessage('', '*');
      },
    });
  1. Sana will then hand this configuration data as a text string to the extension add-on to decode it. This is done via the call to OnConfiguratorClientMessageReceivedAsync method.

  2. Once the extension add-on has received the message it is expected to decode it and return which product ID, variant ID, quantity, unit of measure, etc., Sana should add to the shopping cart. This means that the external product configurator application is expected to either create a new product in ERP or choose existing product in ERP as the result of user's actions. This product in ERP does not have to be visible in the web store, neither does it have to be orderable. Sana skips orderability and visibility checks for such product.

  3. Sana then adds the product, that the extension add-on identified, to the shopping cart. From that moment on it is normal checkout process from Sana's point of view.

Note

It is essential that the ConfiguratorModel field in the ERP system is populated with a value for each product that is to be configurable with this extension. If the ConfiguratorModel field is left blank, the configurator will not operate correctly.

Implementation

Start with a new project

Create a new add-on project named "Sana.Extensions.CustomProductConfigurator" as described in the add-on development tutorial.

The "CustomProductConfigurator" constant is the name which will be used in this tutorial, but in real life add-ons, it should be replaced by the name of the product configurator which the add-on integrates with.

Create the extension add-on's class

Create a new class CustomProductConfiguratorExtension inherited from ProductConfiguratorExtension. More information about ProductConfiguratorExtension you can find in ProductConfiguratorExtension reference article.

public class CustomProductConfiguratorExtension : ProductConfiguratorExtension
{
}

Implement configuration class

This step is optional and is only needed if your extension add-on should have configurable settings that the web store administrator should set in Sana Admin.

Create a new class CustomProductConfiguratorSettings inherited from the ExtensionConfiguration and decorate it with ConfigurationKey attribute. This class will be used by Sana as a view-model to configure product configurator extension in Sana Admin. More details about extension configuration class you can find in Extension configuration article.

[ConfigurationKey("CustomProductConfigurator")]
public class CustomProductConfiguratorSettings : ExtensionConfiguration
{
}

Let's add property, which is needed to configure the product configurator extension, to the CustomProductConfiguratorSettings class. You can decorate the properties with data annotation attributes since this class is a model for a view.

[ConfigurationKey("CustomProductConfigurator")]
public class CustomProductConfiguratorSettings : ExtensionConfiguration
{
    [Display(Name = "ConfiguratorUrl")]
    public string ConfiguratorUrl { get; set; }
}

For the sake of simplicity in our example we will use only one configuration value which represents the URL of the external product configurator web site which will be opened in an iframe.

Implement IConfigurable<TConfiguration> interface in CustomProductConfiguratorExtension. Put CustomProductConfiguratorSettings class as a generic type parameter for IConfigurable<TConfiguration>, it will indicate that our product configurator extension should be configured with this class.

public class CustomProductConfiguratorExtension : ProductConfiguratorExtension, IConfigurable<CustomProductConfiguratorSettings>
{
    public CustomProductConfiguratorSettings Configuration { get; set; }
}

Sana will initialize Configuration property with configuration settings entered in Sana Admin on the extension configuration page. This page will be accessible when you go to all installed extensions page in Sana Admin and click "Configure" button of our "Test product configurator" extension once it gets built and installed in Sana Admin:

Extension Configure Button

Extension Configuration Page

Implement ProductConfiguratorExtension.ConfiguratorId property

This property should specify a unique identifier by which Sana will reference this product configurator add-on in the system.

So let's add ConfiguratorId property implementation to the class:

public class CustomProductConfiguratorExtension : ProductConfiguratorExtension, IConfigurable<CustomProductConfiguratorSettings>
{
    public CustomProductConfiguratorSettings Configuration { get; set; }

    public override string ConfiguratorId => "CustomConfigurator";
}

Implement ProductConfiguratorExtension.DetectConfigurableProductAsync method

Sana calls this method when it needs to know whether a product is configurable by this product configurator add-on or not, so that it can render either an "Add to cart" or a "Configure" button on the product page.

Add the method to the class:

public override Task<ConfigurableProductDetectionResult> DetectConfigurableProductAsync(ConfigurableProductDetectionContext productContext)
{
}

The method returns an instance of Task<> and Sana will call it asynchronously. So in case your implementation requires you to access external resource to perform this check, you may mark the method as async and use await inside it to wait for the external resource call asynchronously.

Input

ConfigurableProductDetectionContext instance that is passed as a parameter, contains ProductId and ConfiguratorModel values, by which you can detect whether the product is configurable by your configurator add-on.

ConfigurableProductDetectionContext.ConfiguratorModel value comes from the product entity - it is a new textual field in ERP, which was added in the latest release specifically to support product configurators. So if you want to mark products as configurable by your extension add-on, you should fill this field in ERP with values that you will detect here in this method.

For example, let's say we have three products in ERP. One of them is not configurable, just a normal product. Another one is configurable by our extension add-on, and the third one is configurable by some third-party product configurator extension add-on. Let's say, we decided to detect products by "my-custom-configurator:" prefix present in the ConfiguratorModel value. This is how these three products would look like in ERP:

Product ID ConfiguratorModel field
"Prod-01" ""
"Prod-02" "my-custom-configurator: my-model"
"Prod-03" "some other value"

Then the implementation of the check should be something like this:

// Here we're checking whether the value of "ConfiguratorModel" from the
// product contains the "my-custom-configurator:" prefix.
bool isConfigurable = productContext.ConfiguratorModel != null && productContext.ConfiguratorModel.StartsWith("my-custom-configurator:");

Or, perhaps your implementation depends on an external web service, or any other external application or resource, to which you pass the product ID and get the response telling you whether the product is recognized or not as configurable by your extension add-on. In such case the implementation may look something like this:

// Here we're passing "ProductId" to the external service asynchronously
// and awaiting the response from it.
bool isConfigurable = await SomeExternalWebService.IsProductConfigurableAsync(productContext.ProductId);

See also ConfigurableProductDetectionContext reference.

Output

The output of the method should be one of the two possible options: either "success" - when your extension add-on indeed recognizes the product as configurable; or "failure" - when it doesn't, respectively.

For "success" scenario you should return the instance of ConfigurableProductDetectionResult instantiated with Success method:

return ConfigurableProductDetectionResult.Success();

For "failure" scenario you return the instance of ConfigurableProductDetectionResult instantiated with Failure method:

return ConfigurableProductDetectionResult.Failure();

See also ConfigurableProductDetectionResult reference.

Here's our complete example, where we detect configurable product if it has "my-custom-configurator:" prefix in its "ConfiguratorModel" field's value:

public override Task<ConfigurableProductDetectionResult> DetectConfigurableProductAsync(ConfigurableProductDetectionContext productContext)
{
    bool isConfigurable = productContext.ConfiguratorModel != null && productContext.ConfiguratorModel.StartsWith("my-custom-configurator:");
    var result = isConfigurable
        ? ConfigurableProductDetectionResult.Success()
        : ConfigurableProductDetectionResult.Failure();
    return Task.FromResult(result);
}

Implement ProductConfiguratorExtension.GetConfiguratorUrlAsync method

When the user clicks "Configure" button on the product details page, Sana needs to open the external product configurator web site in an iframe on the page. For this it needs to know the URL of the configurator web site to set as the value of the src attribute of the iframe.

This is when Sana calls ProductConfiguratorExtension.GetConfiguratorUrlAsync. It must return the URL of the actual configurator web site, including all URL parameters.

Input

The input parameter is ProductConfiguratorContext instance, which contains all needed values that the external configurator web site may need. You may use those values to construct the URL.

See also ProductConfiguratorContext reference.

Output

This method is called by Sana asynchronously, so that in case you need to access external resources to generate the URL, you may mark the method as async. The return value is the Task<string> that will return the URL when complete.

Example:

public override Task<string> GetConfiguratorUrlAsync(ProductConfiguratorContext context)
{
    var uri = new UriBuilder(Configuration.ConfiguratorUrl);
    var parameters = HttpUtility.ParseQueryString("");
    parameters["configuratorModel"] = context.ConfiguratorModel;
    parameters["quantity"] = context.Quantity.ToString();
    parameters["unitOfMeasureId"] = context.UnitOfMeasureId;
    parameters["productId"] = context.ProductId;

    uri.Query = parameters.ToString();

    return Task.FromResult(uriBuilder.Uri.AbsoluteUri);
}

Implement ProductConfiguratorExtension.OnConfiguratorClientMessageReceivedAsync method

After the user has finished configuring the product, Sana expects that the iframe, in which the external configurator web site is shown, will send the message in Javacsript through the browser window to the parent. Whenever Sana's Javascript code receives a message from the iframe on this page, it will trigger ProductConfiguratorExtension.OnConfiguratorClientMessageReceivedAsync method and pass the message to it. The extension add-on is expected to decode this message and return a product, which exists in ERP, and which is the actual result of the configuration that the user has performed, and which Sana needs to add to the basket. The product may or may not be visible in the web store, and it may or may not be available in stock in ERP - Sana will skip visibility and availability checks for such products, when the order is checked out.

The ProductConfiguratorExtension.OnConfiguratorClientMessageReceivedAsync method is also used when Sana processes a request from the external configurator web site. In this case the message contains a string representation of the request body.

Input

The input is the message that was received from the external configurator web site, that is shown in the iframe, in the browser window. The message may be any string, it may be just some text or a Json, Sana does not care. It is up to the extension add-on to handle it.

See also ConfiguratorMessageContext reference.

Output

Once again, because this method is asynchronous, you need to return a Task<> instance that will return ProductConfiguratorResult, which encapsulates the existing product in ERP, that is the actual result of the configuration done by the user. And in case you need to access external resources, you may use async/await here.

Note

If you do not need to save the configuration result (for example, it was already saved after the post request) you must return null as result.

Example:

public override Task<ProductConfiguratorResult> OnConfiguratorClientMessageReceivedAsync(ConfiguratorMessageContext messageContext)
{
    if (string.IsNullOrEmpty(messageContext.MessageFromConfigurator))
        return Task.FromResult<ProductConfiguratorResult>(null);

    var deserialized = JsonConvert.DeserializeObject<TestProductResult>(messageContext.MessageFromConfigurator);
    var product = new ConfiguredProduct()
    {
        ProductId = deserialized.SelectedProductId,
        VariantId = deserialized.SelectedVariantId,
        UnitOfMeasureId = deserialized.UnitOfMeasureId,
        Quantity = deserialized.Quantity,
        Description = deserialized.Description,
        ImageUrl = deserialized.ImageUrl
    };
    var result = new SingleProductConfiguratorResult(deserialized.SessionId, product);
    return Task.FromResult<ProductConfiguratorResult>(result);
}

internal class TestProductResult
{
    public string SessionId { get; set; }
    public string SelectedProductId { get; set; }
    public string SelectedVariantId { get; set; }
    public decimal? Quantity { get; set; }
    public string UnitOfMeasureId { get; set; }
    public string Description { get; set; }
    public string ImageUrl { get; set; }
}

See also ProductConfiguratorResult reference.

Conclusion

Basically, we're done. The minimal product configurator extension add-on is ready. You may build it and test it, see the add-on creation tutorial for more details on it.

See also