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:
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:
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:
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.
So there are three main parties: Sana, the extension add-on and the external configurator web application.
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.
When Sana gets positive response from one of product configurator extension add-ons, it renders "Configure" button instead of usual "Add to cart" button.
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 iniframe
'ssrc
attribute and thus the user sees the external web application in the popup, where they can configure the product to their liking.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 theiframe
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('', '*');
},
});
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.
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.
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:
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.