Skip to content

Feature library plugin

Overview

The feature library plugin uses the WPaaS feature registry to manage and display all available features in the Investec Online platform. It provides a centralised location for feature teams to register their features and for users to view and access these features. The plugin can be used in both web and mobile platforms. The plugin is responsible for getting the features from the feature registry and managing the access rights of each feature based on a clients' context. It is the brain behind the feature library page. Additionally, the plugin manages favourite features, feature search, and feature filtering.

shell
npm install --save @investec/plugins-feature-library

API calls

The feature library plugin uses the following API calls to interact with the WPaaS feature registry and exposes it to the platform:

ts
import { IAddFavouriteFeature, IFavouriteFeatureApps } from '../types/feature-apps.interface';

export async function getWebFeatures(baseUrl: string) {
  const req = await fetch(
    `${baseUrl}/api/features`,
    {
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  const res = await req.json();
  return res;
}
export async function getMobileFeatures(baseUrl: string) {
  const req = await fetch(
    `${baseUrl}/api/mobile-features`,
    {
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  const res = await req.json();
  return res;
}
export async function getFavouriteFeatures(baseUrl: string, zetaId: string): Promise<IFavouriteFeatureApps[]> {
  const req = await fetch(
    `${baseUrl}/api/favourites/${zetaId}`,
    {
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  const res = await req.json();
  return res;
}
export async function addFavouriteFeature(baseUrl: string, data: IAddFavouriteFeature): Promise<{ metaData: { id: string, featureId: string, clientId: string }, success: boolean}> {
  const req = await fetch(
    `${baseUrl}/api/favourites/add`,
    {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  const res = await req.json();
  return res;
}
export async function removeFavouriteFeature(baseUrl: string, id: string): Promise<{ metaData: { id: string }, success: boolean, }> {
  const req = await fetch(
    `${baseUrl}/api/favourites/remove`,
    {
      method: 'POST',
      body: JSON.stringify({id: id}),
      headers: {
        'Content-Type': 'application/json',
      },
    }
  );

  const res = await req.json();
  return res;
}

WARNING

The platform uses this plugin to get all the features from the feature registry. Without it , the platform will not be able to work.

The API calls are part of the Cloudflare pages project cxt-investec-online in the functions folder.

These API calls are made in the io-core service in the platform.

Environment

The plugin makes use of environment variables to set the context of the feature library page. The environment variables are set the feature library context. The following code snippet shows how the environment variables are set:

ts
export function setFeatureLibraryClientContext(authResponseData:object, portfolioData:object, zaClientData:object) {
  authRes = flattenObject(authResponseData);
  portfolio = flattenObject(portfolioData);
  zaClient = flattenObject(zaClientData);
}

export function setFeatureLibraryAuthResponse(authResponseData:any) {
  authRes = flattenObject({AuthResponse:authResponseData});
}

export function setFeatureLibraryPortfolio(portfolioData:any) {
  portfolio = flattenObject({portfolio:portfolioData});
}

export function setFeatureLibraryZaClient(zaClientData:any) {
  zaClient = flattenObject({zaClient:zaClientData});
}

In order for the feature library to work it requires the sa client data (if applicable) , authResponse and portfolio data to be set in the environment variables.

Implementation guide

Checking feature requirements

The feature library exposes a function checkFeatureRequirements to check the feature requirements to determine if a feature is accessible. Execute this function before rendering and feature in the feature library to determine the access. The following code snippet shows how to use the function:

ts
export function checkFeatureRequirements(requirements: IApplicationRequirements[],key:string) {
  if (authRes) {
    if (requirements.length !== 0) {
      let reqLength = 0;
      requirements.forEach((value: IApplicationRequirements) => {
        switch (value.location) {
          case 'AuthResponse':
            reqLength += checkDataRequirementsFromSource(authRes,value);
            break;
          case 'zaClient':
            reqLength += checkDataRequirementsFromSource(zaClient,value);
            break;
        }

        if(value.location.includes('portfolio')){

          if(value.location === 'portfolio.PrivateBankAccounts'){
            value.location = 'portfolio.PrivateBankZA.PrivateBankAccounts';
          }

          if(value.location === 'portfolio.WiOnlineAccounts'){
            value.location = 'portfolio.WealthAndInvestmentZA.WiOnlineAccounts';
          }

          reqLength += checkDataRequirementsFromSource(portfolio,value);
        }
      });

      if(excludedClientFeatures.includes(key)){
        reqLength += 1;
      }

      FeaturesWithApiRequirements.forEach((value) => {
        if(value.feature === key){
          if(!value.eligible){
            reqLength += 1;
          }
        }
      })

      return reqLength === requirements.length;
    } else {
      return true;
    }
  } else {
    return false;
  }
}

This function depends on the checkDataRequirementsFromSource function to check the data requirements of a feature:

ts
function checkDataRequirementsFromSource(source:any,requirements:IApplicationRequirements){
  let reqLength = 0;
  if(!isEmpty(source)){
    try{
      if (requirements.condition !== undefined) {
        if (requirements.condition.includes('!')) {
          if(Array.isArray(source[requirements.location as keyof typeof source])){
            for (let i = 0; i < source[requirements.location as keyof typeof source].length; i++) {
              const sourceElement = source[requirements.location as keyof typeof source][i];
              if (sourceElement[requirements.flag as keyof typeof sourceElement] !== requirements.condition.replace('!', '')) {
                reqLength += 1;
                break;
              }
            }
          } else {
            if (source[requirements.location +'.'+ requirements.flag as keyof typeof source].toString() !== requirements.condition.replace('!', '')) {
              reqLength += 1;
            }
          }
        } else {
          if(Array.isArray(source[requirements.location as keyof typeof source])){
            for (let i = 0; i < source[requirements.location as keyof typeof source].length; i++) {
              const sourceElement = source[requirements.location as keyof typeof source][i];
              if (sourceElement[requirements.flag as keyof typeof sourceElement] === requirements.condition) {
                reqLength += 1;
                break;
              }
            }
          } else {
            if (source[requirements.location +'.'+ requirements.flag as keyof typeof source].toString() === requirements.condition) {
              reqLength += 1;
            }
          }
        }
      } else {
        if(Array.isArray(source[requirements.location as keyof typeof source])){
          for (let i = 0; i < source[requirements.location as keyof typeof source].length; i++) {
            const sourceElement = source[requirements.location as keyof typeof source][i];
            if (sourceElement[requirements.flag as keyof typeof sourceElement] === true) {
              reqLength += 1;
              break;
            }
          }
        } else {
          if (source[requirements.location +'.'+ requirements.flag as keyof typeof source] === true) {
            reqLength += 1;
          }
        }
      }
    } catch (e) {
      console.warn('feature-library issue',{source,requirements});
    }
  }

  return reqLength;
}

INFO

This function will use whatever context is had been give to determine if the feature can be accessed by the client. By default if it cant find the needed context to meet the requirements, it will return a false. This function checks for various things including negative checks and conditional checks. It is generic in nature in order for the feature library to scale.

Features can be linked to activities from adobe target. The plugin will use the personalisation data from the platform to modify the feature meta-data. This function should be called before the feature library renders out the results:

ts
export function checkAdobeTargetActivities(feature:IFeatureApps,AdobeTarget:any){
  if(feature.value.adobeTargetActivity !== '' && AdobeTarget !== undefined){
    const targetActivity: IAdobeTargetMboxes = findPropertyName(AdobeTarget,feature.value.adobeTargetActivity);
    if(targetActivity !== undefined){
      if(targetActivity.options[0].content.insightsCard !== undefined) {
        feature.value.description.full = targetActivity.options[0].content.insightsCard.content.description
        feature.value.description.short = targetActivity.options[0].content.insightsCard.content.description
        feature.value.name = targetActivity.options[0].content.insightsCard.content.heading
        feature.value.media.banner = targetActivity.options[0].content.insightsCard.content.imageUrl
        feature.value.callToAction.target = targetActivity.options[0].content.insightsCard.content.primaryCallToAction.target
        feature.value.callToAction.text = targetActivity.options[0].content.insightsCard.content.primaryCallToAction.text
      }
    }
  }
  return feature;
}

Additionally, the plugin uses an object for features using API calls to check their access rights:

ts
export const FeaturesWithApiRequirements :{feature:string,eligible:boolean}[] = []

this is kept in memory and reused by the plugin to check if a feature is accessible.

In order for the feature library to execute features using API calls , it exposes a function that must be called in the platform, with some params required to do so:

ts
export function checkFeatureApiRequirements(featureList:IFeatureApps[],profileId?:string){
  return new Promise((resolve) => {
    let numOfCalls = 0;
    let responsesReceived = 0;
    featureList.forEach(feature => {
      if (feature.value.apiRequirements !== undefined && feature.value.apiRequirements?.apiEndpoint !== undefined && feature.value.apiRequirements?.propertyName !== undefined) {
        numOfCalls++;
        if (profileId) {
          feature.value.apiRequirements.apiEndpoint = feature.value.apiRequirements.apiEndpoint.replace('<profileId>', profileId)
        }

        fetch(
          `/proxy/${feature.value.apiRequirements.apiEndpoint}`,
          {
            headers: {
              'Content-Type': 'application/json',
            },
          }
        ).then(res => res.json()).then((req) => {
          responsesReceived++;
          const response = flattenObject(req);

          if (feature.value.apiRequirements.propertyName) {
            if (response[feature.value.apiRequirements.propertyName] === undefined) {
              FeaturesWithApiRequirements.push({ feature: feature.key, eligible: false })
            } else if (typeof response[feature.value.apiRequirements.propertyName] === 'string') {
              FeaturesWithApiRequirements.push({
                feature: feature.key,
                eligible: response[feature.value.apiRequirements.propertyName] === 'true'
              })
            } else {
              FeaturesWithApiRequirements.push({
                feature: feature.key,
                eligible: response[feature.value.apiRequirements.propertyName]
              })
            }
          } else {
            FeaturesWithApiRequirements.push({ feature: feature.key, eligible: false })
          }

          if(responsesReceived === numOfCalls){
            resolve(true);
          }
        }).catch(() => {
          responsesReceived++;
          FeaturesWithApiRequirements.push({ feature: feature.key, eligible: false })
          if(responsesReceived === numOfCalls){
            resolve(true);
          }
        })
      }
    })
    if(responsesReceived === numOfCalls){
      resolve(true);
    }
  })
}

Finally, the plugin can check if a client has been excluded from a feature. This is done by checking if the client zeta id is in the exclusion list of the feature.

The following code snippet shows how the plugin gets the exclusion list of a feature and also returns it to the platform where it can be stored in state and exposed via the SDK.

ts
export function checkExcludedClientFeatures(zetaId:string):Promise<string[]>{
  return new Promise((resolve) => {
    fetch(`/api/exclude/${zetaId}`, { headers: { 'Content-Type': 'application/json'}})
      .then(res => res.json()).then((res) => {
        excludedClientFeatures.push(...res);
        resolve(excludedClientFeatures);
    })
  })
}

This list is kept in memory and reused by the plugin to check if a client is excluded from a feature.

The feature library plugin provides a search functionality to search for features. The following code snippet shows how to use the search functionality:

ts
export function featureSearch(searchTerm: string, features: any): any {
  const options = {
    includeScore: true,
    keys: ['value.name', 'value.description.short', 'value.description.full', 'value.tags', 'value.keywords'],
    threshold: 0.3,
  };

  const fuse = new Fuse(features, options);

  const results = fuse.search(searchTerm);
  return results.map((result:any) => {
    return result.item;
  });
}

export function featureSearchNoStrict(searchTerm: string, features: any): any {
  const options = {
    includeScore: true,
    keys: ['value.name', 'value.description.short', 'value.description.full', 'value.tags', 'value.keywords'],
    threshold: 0.8,
  };

  const fuse = new Fuse(features, options);

  const results = fuse.search(searchTerm);
  return results.map((result:any) => {
    return result.item;
  });
}

INFO

The platform feature library service uses all the above-mentioned functions to check the feature requirements and search for features.