Customize Storefront Widgets

You must customize two widgets and add them to your storefront layouts so that shoppers can view and work with subscriptions assigned to their accounts.

This chapter describes how to customize the following widgets:

  • The customized Assets widget lets shoppers view all their subscriptions and each subscription’s line items. This widget uses the Subscriptions-app SSE module to fetch the details from Oracle Subscription Cloud.
  • The customized Asset Details widget lets a shopper modify, renew, cancel, or change a subscription. This widget uses the Subscription-assets-store SSE module to perform some of the predefined asset actions, such as renew, modify, terminate, suspend, or resume.

This chapter describes the changes you need to make to the Assets and Asset Details widgets so they can display subscriptions. For information about how to access the code for default widgets so you can edit it, see Customize layout components in Using Oracle Commerce.

Note: The SSEs must be installed and configured before you can customize the widgets and use them on the storefront.

Customize the Assets widget

The Assets widget lets shoppers view a list of services associated with their account. You can customize this widget so that it displays a list of subscriptions for the logged in user.. This widget uses the Subscriptions-app server-side extension module to fetch the details from Oracle Subscription Cloud. The Assets widget appears on the Assets layout.

The following illustration shows a shopper’s subscriptions, displayed in the widget.

This image is described in the surrounding text.

When the shopper clicks the Show Products button, the Subscription Asset Details widget displays in the Asset Details layout.

Customize the Assets widget code

By default, the Assets widget shows the root level assets on the assets page and clicking on the Details link redirects the shopper to the asset details. For this integration, you will customize the widget to show the subscriptions on the page first, then give the shopper the option to view subscription products. By checking details of a particular subscription product, the shopper will be directed to the root asset details associated with the product.

To make these changes to the Assets widget, update the widget’s .js file.

First, add/update the following constants:

  var GET_ALL_SUBSCRIPTIONS = "getAllSubscription";
  var GET_ALL_SUBSCRIPTION_PRODUCTS = "getAllSubscriptionProducts";
  var ENDPOINT_VIEW_ACCOUNT_ASSET =    "getServices";

Next, add the following observables:

currentSubscriptionNumber: ko.observable(), productPageSize:
      ko.observableArray([]), productsPageSize:
      ko.observable(ccConstants,DEFAULT_SEARCH_RECORDS_PER_PAGE || 12), productsOffset:
      ko.observable(0) productshasMore: ko.observable(false), productsTotalResults:
      ko.observable(0), showProductsFlag: ko.observable(false),

Next, update the beforeAppear function:

beforeAppear: function (page) {
      var widget = this;

      if ( !this.user().loggedIn() ) {
        navigation.doLogin(navigation.getPath(), this.links().home.route);
      }
      widget.showProductsFlag(false);
    }

Finally, add/update the following methods inside the onload function:

        onLoad: function (widget) {
      // Add the Services SSE endpoints to the ccRestClient endpoint registry.
      // Update the settings below if the Services SSE has been customized.
      // ENSURE THAT THE SERVICES SSE IS INSTALLED, CONFIGURED AND AVAILABLE
      ccRestClient.registerInitCallback(function(){

        ccRestClient.endpointRegistry[ENDPOINT_VIEW_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_VIEW_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "GET",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry.getAllSubscription = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": false,
          "httpsRequired": false,
          "id": GET_ALL_SUBSCRIPTIONS,
          "localeHint": "assetLanguageOptional",
          "method": "GET",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/oss/subscription/getAll",
          "useOptimisticLock": false
        }

        ccRestClient.endpointRegistry.getAllSubscriptionProducts = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": false,
          "httpsRequired": false,
          "id": GET_ALL_SUBSCRIPTION_PRODUCTS,
          "localeHint": "assetLanguageOptional",
          "method": "GET",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/oss/subscription/products",
          "useOptimisticLock": false
        }

      });
            // Use the widget's asetsPerPage config
      // option value if it has been set
      if ( widget.assetsPerPage && !isNaN(widget.assetsPerPage()) ) {
        widget.pageSize(10);
      }

      // Computeds for paging control
      widget.totalPages =  ko.pureComputed(function() {
        var returnValue = Math.ceil( widget.totalResults() / widget.pageSize() );
        return returnValue;
      });

      widget.currentPage = ko.pureComputed(function() {
        var returnValue = Math.ceil( ( widget.offset() + widget.pageSize() ) / widget.pageSize() );
        return returnValue;
      });

      widget.previousPage = ko.pureComputed(function() {
        var calculatedPreviousPage = widget.currentPage() - 1;
        var returnValue = ( ( calculatedPreviousPage < 1 ) ? 1 : calculatedPreviousPage );
        return returnValue;
      });

      widget.nextPage = ko.pureComputed(function() {
        var calculatedNextPage = widget.currentPage() + 1;
        var returnValue = ( ( calculatedNextPage > widget.totalPages ) ? widget.totalPages : calculatedNextPage );
        return returnValue;
      });

      widget.onFirstPage = ko.pureComputed(function() {
        return ( widget.currentPage() === 1 );
      });

      widget.onLastPage = ko.pureComputed(function() {
        var returnValue = false;

        if ( widget.totalPages() > 1 )  {
          if ( widget.currentPage() === widget.totalPages() ) {
            returnValue = true;
          }
        }
        else if ( !widget.onFirstPage() && !widget.hasMore() ) {
          returnValue = true;
        }

        return returnValue;
      });

      widget.pageLinks = ko.pureComputed(function() {
        // This would be a good place to do something more
        // sensible with the individual page links that
        // are displayed when there are a large number of
        // results e.g. could display just the 5 pages either
        // side of the current page. For now display every
        // individual page.
        var links = [];

        for (var i = 1; i <= widget.totalPages(); i++) {
          links.push({
            pageNumber : i,
            active : i === widget.currentPage()
          });
        }

        return links;
      });

  widget.shouldShowGoToLastPage = ko.pureComputed(function() {
        return ( widget.totalPages() > 1 );
      });

      widget.isPagingRequired = ko.pureComputed(function() {
        var returnValue = false;

        if ( widget.totalPages() > 1 ) {
          returnValue = true;
        }
        else if ( widget.hasMore() ) {
          returnValue = true;
        }
        else if ( !widget.onFirstPage() ) {
          returnValue = true;
        }

        return returnValue;
      });

      // The goToPage function handles click events from the
      // template's paging links. It takes a single input
      // parameter - pageNumber - which indicates the page
      // of assets to load. If the REST call is successful
      // the assets observable is updated with the returned
      // data and the three paging observables (offset, hasMore
      // and totalResults) are updated with the respective
      // values returned from the REST call.
      widget.goToPage = function (pageNumber) {

        function success (data) {
          var productQuery = '';
          var productIdsSet =  new Set();

            widget.assets(data.items);
            widget.offset(data.offset);
            widget.hasMore(data.hasMore);
            // if totalPages is not returned (i.e. is null)
            // then set to -1; in this scenario only simple
            // paging will be available (i.e. go to first page,
            // go to previous page and go to next page) and
            // the hasMore value will be used to control
            widget.totalResults(data.totalResults || -1);

            spinner.destroyWithoutDelay(widget.spinnerOptions.parent);
         // }

        }

        function error (data) {
          if (data.status == ccConstants.HTTP_UNAUTHORIZED_ERROR) {
            widget.user().handleSessionExpired();

            navigation.doLogin(navigation.getPath, widget.links().home.route);

          } else {
            navigation.goTo(widget.links().profile.route);
          }

          spinner.destroyWithoutDelay(widget.spinnerOptions.parent);
        }

        if ( widget.user().loggedIn() ) {
          var calculatedOffset = ( pageNumber - 1 ) * widget.pageSize();

          spinner.create(widget.spinnerOptions);
          var queryString = 'Status=ORA_ACTIVE';
          var payload = {
            limit: 10,
            offset:calculatedOffset,
            q: queryString,
            orderBy:"CreationDate:desc"
          };

          ccRestClient.request(
           GET_ALL_SUBSCRIPTIONS,
           payload,
           success,
           error
          );
        }
      };

      widget.handleQuickViewClick = function(pIsModal) {
        var popup;
        if(pIsModal === true && this.productDetails) {
          widget.productDetails(this.productDetails);
          popup = $("#cc-upgrade-asset-display");
          popup.modal('show');
        }
      };

      //---------------------------------------------------//
        //Function Specific to subscription products table
        widget.productTotalPages = ko.pureComputed(function(){
          var returnValue = Math.ceil(widget.productsTotalResults()/widget.productPageSize())
          return returnValue;
        });

        widget.productCurrentPage = ko.pureComputed(function(){
          var returnValue = Math.ceil((widget.productsOffset()+widget.pageSize())/widget.pageSize());
          return returnValue;
        });

        widget.previousProductPage = ko.pureComputed(function() {
          var calculatedPreviousPage = widget.productCurrentPage()-1;
          var  returnValue = ( ( calculatedPreviousPage < 1 ) ? 1 : calculatedPreviousPage );
          return returnValue;
        });

        widget.nextProductPage =  ko.pureComputed(function() {
          var calculatedNextPage = widget.productCurrentPage() + 1;
          var returnValue = ( ( calculatedNextPage > widget.productTotalPages ) ? widget.productTotalPages : calculatedNextPage );
          return returnValue;
        });

        widget.onProductFirstPage = ko.pureComputed(function() {
          return ( widget.productCurrentPage() === 1 );
        });

        widget.onProductLastPage = ko.pureComputed(function() {
          var returnValue = false;

          if ( widget.productTotalPages() > 1 )  {
            if ( widget.productCurrentPage() === widget.productTotalPages() ) {
              returnValue = true;
            }
          }
          else if ( !widget.onProductFirstPage() && !widget.productshasMore() ) {
            returnValue = true;
          }

          return returnValue;
        });

        widget.shouldShowGoToProductsLastPage = ko.pureComputed(function() {
          return ( widget.productTotalPages() > 1 );
        });

        widget.isProductPagingRequired = ko.pureComputed(function() {
          var returnValue = false;

          if ( widget.productTotalPages() > 1 ) {
            returnValue = true;
          }
          else if ( widget.productshasMore() ) {
            returnValue = true;
          }
          else if ( !widget.onFirstPage() ) {
            returnValue = true;
          }

          return returnValue;
        });

        widget.productPageLinks = ko.pureComputed(function() {
          var links = [];

          for (var i = 1; i <= widget.productTotalPages(); i++) {
            links.push({
              pageNumber : i,
              active : i === widget.productCurrentPage()
            });
          }

          return links;
        });

         widget.goToProductPage = function(pageNumber) {
          function showProductSuccess(data) {
            widget.subscriptionProducts(data.items);
            widget.productsOffset(data.offset);
            widget.productshasMore(data.hasMore);
            widget.productsTotalResults(data.totalResults || -1);
            spinner.destroyWithoutDelay(widget.spinnerOptions.parent);
          }

          function showProductFailure(err) {
            if (data.status == ccConstants.HTTP_UNAUTHORIZED_ERROR) {
              widget.user().handleSessionExpired();

              navigation.doLogin(navigation.getPath, widget.links().home.route);

            } else {
              navigation.goTo(widget.links().profile.route);
            }

            spinner.destroyWithoutDelay(widget.spinnerOptions.parent);
          }

          if (widget.user().loggedIn()){
            var calculatedOffset = ( pageNumber - 1 ) * widget.pageSize();
            //let queryString = `SubscriptionNumber=${widget.currentSubscriptionNumber()};Status=ORA_ACTIVE;ExternalParentAssetKey is NULL`;
            //var queryString = "SubscriptionNumber=" + widget.currentSubscriptionNumber();
           var queryString = "SubscriptionNumber=" + widget.currentSubscriptionNumber();
            var payload = {
              limit: 25,
              offset:calculatedOffset,
              q: queryString,
              orderBy:"StartDate:desc"
            };
            ccRestClient.request(
              GET_ALL_SUBSCRIPTION_PRODUCTS,
              payload,
              showProductSuccess,
              showProductFailure
            );
          }
        };

        widget.onShowProductsClicked = function(p1, p2) {
          widget.subscriptionProducts([]);
          widget.currentSubscriptionNumber(p1.SubscriptionNumber);
          widget.showProductsFlag(true);
          spinner.create(widget.spinnerOptions);
          widget.goToProductPage(1);

        }

        widget.backToSubscriptionTable = function(p1, p2) {
          widget.showProductsFlag(false);
        }

        widget.redirectToAssetDetailsPage = function(p1, p2) {
          //let assetKeys = p1.ExternalAssetKey;
          var assetKeys = null;
          if(p1.ExternalRootAssetKey) {
              assetKeys= p1.ExternalRootAssetKey;
          } else {
            assetKeys = p1.ExternalAssetKey;
          }
          var payload = {
           // q: queryString,
            limit: 10,
            offset:0,
            assetKeys:assetKeys
          };

          // Call WAPI.
          ccRestClient.request(
           ENDPOINT_VIEW_ACCOUNT_ASSET,
           payload,
           widget.getAssetDetailsSuccess,
           widget.getAssetDetailsError
          );
        };

        widget.getAssetDetailsSuccess = function(data) {
          /* if(data && data.items) {
              data.items.forEach(function(item){
                $.extend(item, {
                  route:
                  `${widget.links().assetDetails.route}/${item.assetId}`
                })
              })
          } */
          if(data.items.length>0)
            navigation.goTo(widget.links().assetDetails.route  + "/" + data.items[0].assetId);
          else
            navigation.goTo(widget.links().profile.route);
        };

        widget.getAssetDetailsError = function(data) {
          if (data.status == ccConstants.HTTP_UNAUTHORIZED_ERROR || data.status == ccConstants.BAD_REQUEST) {
            widget.user().handleSessionExpired();
            navigation.doLogin(navigation.getPath, widget.links().home.route);
          }
          else {
            navigation.goTo(widget.links().profile.route);
          }
          spinner.destroyWithoutDelay(widget.spinnerOptions.parent);
        };

Customize the Asset Details widget

The Asset Details widget lets an account-based contact perform asset-based ordering actions on asset. This customized widget uses the Subscription-assets-store server-side extension module to perform some of the predefined asset actions. This widget appears on the Asset Details layout.

  • The widget’s display.template file includes predefined action buttons that let shoppers perform subscription upgrades, renewals, modifications, cancellations, and suspends/resumes. You can enable or disable these options, based on the asset status or rules you have defined in Oracle CPQ. The widget uses the subscription-service-assets-details SSE to perform these actions.
  • Once a shopper clicks one of the action buttons, the asset is added to their cart with the required action code or a configurator window opens with asset details.
  • The actions are performed based on the asset’s ID, which the SSE endpoints fetch from CPQ. The following illustration shows the details of a subscription, displayed in the widget.

The following illustration shows the details of a subscription, displayed in the widget.

This image is described in the surrounding text.

Customize the Asset Details widget

To customize the Asset Details widget, you must update the endpoint references. All the REST API paths must be changed from /ccstorex/custom/v1/services* to /ccstorex/custom/v1/assets*.

A total of seven endpoints must be updated. In the widget’s asset_details.js file. replace the call to ccRestClient.registerInitCallback() in the onLoad() function with the following:

    onLoad: function (widget) {

      // Add the Subscription-assets-store SSE endpoints to the ccRestClient endpoint registry.
      // Update the settings below if the SSE has been customized.
      // ENSURE THAT THE SSE IS INSTALLED, CONFIGURED AND AVAILABLE

      ccRestClient.registerInitCallback(function(){

        ccRestClient.endpointRegistry[ENDPOINT_VIEW_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_VIEW_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "GET",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_RENEW_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_RENEW_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "POST",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}/renew",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_MODIFY_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_MODIFY_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "POST",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}/modify",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_TERMINATE_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_TERMINATE_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "POST",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}/terminate",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_SUSPEND_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_SUSPEND_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "POST",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}/suspend",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_RESUME_ACCOUNT_ASSET] = {
          "authRequired": true,
          "cachingEnabled": false,
          "hasDoc": false,
          "hasPathParams": true,
          "httpsRequired": false,
          "id": ENDPOINT_RESUME_ACCOUNT_ASSET,
          "localeHint": "assetLanguageOptional",
          "method": "POST",
          "requestType": "application/json",
          "responseType": "application/json",
          "singular": false,
          "url": "/ccstorex/custom/v1/assets/{}/resume",
          "useOptimisticLock": false
        };

        ccRestClient.endpointRegistry[ENDPOINT_UPGRADE_ACCOUNT_ASSET] = {
                "authRequired": true,
                "cachingEnabled": false,
                "hasDoc": false,
                "hasPathParams": true,
                "httpsRequired": false,
                "id": ENDPOINT_UPGRADE_ACCOUNT_ASSET,
                "localeHint": "assetLanguageOptional",
                "method": "POST",
                "requestType": "application/json",
                "responseType": "application/json",
                "singular": false,
                "url": "/ccstorex/custom/v1/assets/{}/upgrade",
                "useOptimisticLock": false
              };

      });

Customize text in the widgets

In addition to updating the widget’s code, you can optionally modify its display text so that shoppers understand that the widgets specifically show details about subscriptions. For example, you could change the word services to subscriptions wherever it appears in widget text. To learn how to modify the text a widget displays, see Modify a component’s code in Using Oracle Commerce.