Skip to main content

Script Injection

We’ll need to inject Javascript into the theme to show the product highlights and price drop label. Fynd Platform Theme engine is built with Vue2, that’s why we will have to create an Injectable script in Vue itself.

info

Fynd Platform now supports React themes as well. For more information on theme development, please refer to the documentation.

Step 1: Create Binding in Extension

  1. Visit Fynd Partners.

  2. Click on Manage and Select your organization.

  3. Click on Extensions from the left Menu.

  4. Select your Extension.

  5. Click on three dots and then select Bindings as shown in the below image.

    click on bindings

  6. Click on Add Template and fill in the data as shown in the below image.

    create binding for product highlights

  7. Repeat Step 6 for the Price Drop Tag also.

    create binding for price drop

    binding list page

Now in the next step, we’ll create Vue components and inject them using these bindings.

Step 2: Setup Vue project

  • Install vue-cli npm package using the following command

    npm i -g @vue/cli
  • Create new directory binding inside your project root directory using the following command

    mkdir bindings
    cd bindings
  • Create Vue project

    vue create .
    note

    Choose Yes for the prompt “Generate project in the current directory?” and select Vue2 for the prompt “Please pick a preset:”

  • Open /bindings/vue.config.js file and replace the code with the following code

    module.exports = {
    pages: {
    index: {
    // entry for the page
    entry: process.env.NODE_ENV == "development" ? 'src/dev.js' : 'src/main.js'
    },
    },
    devServer: {
    disableHostCheck: true
    }
    }
  • Install axios and url-join package inside /bindings/ directory using the following commands

    npm i axios
    npm i url-join
  • In the /binding/ directory, open the /package.json file and replace the following line

    "build": "vue-cli-service build",

    with

    "build": "vue-cli-service build --target lib src/main.js --name product-highlights",
  • Delete unnecessary files such as README.md and jsconfig.json files from the /bindings/ directory

  • Delete all the files and folders from the /bindings/src directory

  • In the /bindings/src/ directory, create a new file called dev.js and add the following code to the file:

    import App from './App.vue';
    import Vue from 'vue';

    new Vue({
    render: h => h(App)
    }).$mount('#app')
  • In the /bindings/src/ directory, create a new file called main.js and add the following code to the file:

    import Highlights from './Highlights.vue'
    import PriceDrop from './PriceDrop.vue'

    window.FPI.extension.register("#product-highlights", {
    mounted(element) {
    window.FPI.extension.mountApp({
    element,
    component: Highlights
    });
    }
    })

    window.FPI.extension.register("#product-price-drop", {
    mounted(element) {
    window.FPI.extension.mountApp({
    element,
    component: PriceDrop
    })
    }
    })

Step 3: Create Vue components

  • In the /bindings/src/ directory, create a new file called Highlights.vue and add the following code to the file:

    <template>
    <div class="product-highlights">
    <div v-if="highlightsData">
    <div class="highlightTitle">Product Highlights</div>
    <div
    v-for="(highlight, index) in highlightsData"
    :key="index"
    >
    <div class="highlightList">{{highlight}}</div>
    </div>
    </div>
    </div>
    </template>

    <script>
    import axios from 'axios';
    import urlJoin from "url-join";

    export default {
    name: "ProductHighlights",

    data() {
    return {
    highlightsData: null
    };
    },

    async mounted() {
    const baseURL = window.location.origin;
    const product_slug = this.$route.params.slug;


    let { data } = await axios.get(
    urlJoin(baseURL, 'ext/producthighlights/highlight'),
    { params: {slug: product_slug}, headers: {"ngrok-skip-browser-warning": true} }
    );

    if (data && data.is_active && data.product && data.product.highlights && data.product.highlights.length) {
    this.highlightsData = data.product.highlights
    }
    },
    }
    </script>

    <style>
    .highlightTitle {
    font-size: 14px;
    font-weight: 700;
    color: #000000;
    padding: 32px 0 6px 0;
    }

    .highlightList {
    font-size: 14px;
    font-weight: 400;
    color: #000000;
    padding: 8px 0;
    }
    </style>
    note

    "ngrok-skip-browser-warning” request header is added to skip the “You are about to visit: ngrok_url.free.app” page. for the API call. You can remove that header in the production build.

  • Download drop-price-tag.svg file from GitHub and add it into the /bindings/assets/ directory. Here is the link to SVG : Click Here

  • In the /bindings/src/ directory, create a new file called PriceDrop.vue and add the following code to the file:

    <template>
    <div>
    <div v-if="showPriceDrop">
    <img src="../assets/drop-price-tag.svg" alt="price-drop-png" />
    </div>
    </div>
    </template>

    <script>
    import axios from "axios";
    import urlJoin from "url-join";
    export default {
    name: 'PriceDrop',

    data() {
    return {
    showPriceDrop: false
    }
    },

    async mounted() {
    const baseURL = window.location.origin;
    const product_slug = this.$route.params.slug;

    let { data } = await axios.get(
    urlJoin(baseURL, 'ext/producthighlights/price-drop'),
    { params: {slug: product_slug}, headers: { "ngrok-skip-browser-warning": true } }
    );

    if (data && data.showPriceDrop) {
    this.showPriceDrop = true;
    }
    }

    }
    </script>

    <style scoped>
    img {
    height: 48px;
    width: auto;
    }
    </style>

    note

    “ngrok-skip-browser-warning” request header is added to skip the “You are about to visit: ngrok_url.free.app” page. for the API call. You can remove that header in the production build.

  • Build the project using the following command inside /bindings/ directory.

    npm run build

Step 4: Write logic for script injection

  • In the pom.xml file, add following code in <build> tag

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <executions>
    <execution>
    <id>Copy my VueJS app into my Spring Boot target static folder</id>
    <phase>process-resources</phase>
    <goals>
    <goal>copy-resources</goal>
    </goals>
    <configuration>
    <outputDirectory>target/classes/static</outputDirectory>
    <resources>
    <resource>
    <directory>app/build</directory>
    <filtering>true</filtering>
    </resource>
    <resource>
    <directory>bindings/dist</directory>
    <filtering>true</filtering>
    </resource>
    </resources>
    </configuration>
    </execution>
    </executions>
    </plugin>
    </plugins>
    </build>
  • Also update the /src/main/java/com/fynd/example/java/controller/RouteController.java file

    package com.fynd.example.java.controller;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;

    @Controller
    @CrossOrigin(origins = "*")
    public class RouteController {

    @RequestMapping("/company/{company_id}")
    public String redirect() {
    return "forward:/";
    }

    @RequestMapping("/bindings/product-highlights/{static_file}")
    public String bindings(
    @PathVariable("static_file") String staticFile
    ) {
    return "forward:/"+staticFile;
    }
    }

    with these we are serving min.js files which we have created by building the bindings bundle. Now these min.js files can be accessed using Extension base URL.

  • To maintain the record of the Proxy we will create one MongoDB schema. for that open the /com/fynd/example/java/db/Proxy.java file and add the following code

    package com.fynd.example.java.db;

    import com.fasterxml.jackson.annotation.JsonProperty;
    import org.springframework.data.mongodb.core.mapping.Document;
    import lombok.*;

    @Document(collection = "proxy")
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class Proxy {

    @JsonProperty("company_id")
    private String companyId;

    @JsonProperty("application_id")
    private String applicationId;

    @JsonProperty("attached_path")
    private String attachedPath;

    @JsonProperty("proxy_url")
    private String proxyUrl;
    }
  • Also, create ProxyRepository interface inside /com/fynd/example/java/db/interfaces directory

    package com.fynd.example.java.db.interfaces;

    import com.fynd.example.java.db.Proxy;
    import org.springframework.data.mongodb.repository.MongoRepository;
    import org.springframework.stereotype.Repository;

    @Repository
    public interface ProxyRepository extends MongoRepository<Proxy, String> {

    long countByCompanyIdAndApplicationId(String companyId, String applicationId);

    long deleteByCompanyIdAndApplicationId(String companyId, String applicationId);

    long deleteByCompanyId(String companyId);
    }
  • Create /com/fynd/example/java/helper/utils/TagSchemaUtils.java util class to get tag schema

    package com.fynd.example.java.helper.utils;

    import com.fynd.extension.model.ExtensionProperties;
    import com.sdk.platform.content.ContentPlatformModels.CreateTagSchema;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    import java.util.*;

    @Component
    public class TagSchemaUtils {

    @Autowired
    ExtensionProperties extensionProperties;

    public List<CreateTagSchema> getTagSchema(String applicationId) {
    List<CreateTagSchema> tagSchemas = new ArrayList<>();

    CreateTagSchema tagSchema1 = new CreateTagSchema();
    tagSchema1.setName("Product Highlights injection script");
    tagSchema1.setSubType("external");
    tagSchema1.setType("js");
    tagSchema1.setPosition("body-bottom");
    tagSchema1.setUrl(extensionProperties.getBaseUrl() + "/bindings/product-highlights/product-highlights.umd.js");
    Map<String, String> attributes = new HashMap<>();
    attributes.put("id", applicationId);
    tagSchema1.setAttributes(attributes);
    tagSchemas.add(tagSchema1);

    CreateTagSchema tagSchema2 = new CreateTagSchema();
    tagSchema2.setName("Product Highlight injection css");
    tagSchema2.setSubType("external");
    tagSchema2.setType("css");
    tagSchema2.setPosition("head");
    tagSchema2.setUrl(extensionProperties.getBaseUrl() + "/bindings/product-highlights/product-highlights.css");
    tagSchema2.setAttributes(new HashMap<>());
    tagSchemas.add(tagSchema2);

    return tagSchemas;
    }
    }
  • In the /src/main/java/com/fynd/example/java/controller package, create a new class called ScriptController.java and add the following code to the file:

    package com.fynd.example.java.controller;

    import com.fynd.example.java.db.ProductHighlight;
    import com.fynd.example.java.db.Proxy;
    import com.fynd.example.java.db.interfaces.ProductHighlightRepository;
    import com.fynd.example.java.db.interfaces.ProxyRepository;
    import com.fynd.example.java.helper.utils.TagSchemaUtils;
    import com.fynd.example.java.properties.Config;
    import com.fynd.extension.controllers.BasePlatformController;
    import com.fynd.extension.model.ExtensionProperties;
    import com.fynd.extension.session.Session;
    import com.sdk.common.model.FDKException;
    import com.sdk.common.model.FDKServerResponseError;
    import com.sdk.platform.PlatformClient;
    import com.sdk.platform.content.ContentPlatformModels.TagsSchema;
    import com.sdk.platform.content.ContentPlatformModels.CreateTagRequestSchema;
    import com.sdk.platform.partner.PartnerPlatformModels.AddProxyReq;
    import jakarta.servlet.http.HttpServletRequest;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;

    import java.util.Optional;

    @RestController
    @RequestMapping("/api/v1.0")
    @Slf4j
    public class ScriptController extends BasePlatformController {

    @Autowired
    ProductHighlightRepository productHighlightRepository;

    @Autowired
    ProxyRepository proxyRepository;

    @Autowired
    ExtensionProperties extensionProperties;

    @Autowired
    TagSchemaUtils tagSchemaUtils;

    @Autowired
    Config config;

    @PostMapping("/{application_id}/tag/{item_code}")
    public ResponseEntity addScript(
    @PathVariable("application_id") String applicationId,
    @PathVariable("item_code") Integer itemCode,
    HttpServletRequest request
    ) throws FDKServerResponseError, FDKException {
    Session fdkSession = (Session) request.getAttribute("fdkSession");
    String companyId = fdkSession.getCompanyId();
    PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");

    TagsSchema response;

    Optional<ProductHighlight> data = productHighlightRepository.findOneByCompanyIdAndApplicationIdAndProductItemCode(companyId, applicationId, itemCode);

    if (data.isPresent()) {
    ProductHighlight productHighlight = data.get();
    productHighlight.setIsActive(true);
    productHighlightRepository.save(productHighlight);

    long proxyCount = proxyRepository.countByCompanyIdAndApplicationId(companyId, applicationId);

    if (proxyCount < 1) {
    AddProxyReq body = new AddProxyReq(config.getProxyAttachPath(), extensionProperties.getBaseUrl()+"/app/proxy");
    platformClient.application(applicationId).partner.addProxyPath(extensionProperties.getApiKey(), body);

    Proxy proxy = new Proxy(
    companyId,
    applicationId,
    config.getProxyAttachPath(),
    extensionProperties.getBaseUrl()+"/app/proxy"
    );
    proxyRepository.save(proxy);
    }

    response = platformClient.application(applicationId).content.addInjectableTag(
    new CreateTagRequestSchema(tagSchemaUtils.getTagSchema(applicationId))
    );

    } else {
    throw new RuntimeException("Invalid Item Code: " + itemCode);
    }

    return ResponseEntity.status(HttpStatus.OK).body(response);
    }

    @DeleteMapping("/{application_id}/tag/{item_code}")
    public ResponseEntity deleteScript(
    @PathVariable("application_id") String applicationId,
    @PathVariable("item_code") Integer itemCode,
    HttpServletRequest request
    ) throws FDKServerResponseError, FDKException {
    Session fdkSession = (Session) request.getAttribute("fdkSession");
    String companyId = fdkSession.getCompanyId();
    PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");

    Optional<ProductHighlight> data = productHighlightRepository.findOneByCompanyIdAndApplicationIdAndProductItemCode(companyId, applicationId, itemCode);

    if (data.isPresent()) {
    ProductHighlight productHighlight = data.get();
    productHighlight.setIsActive(false);
    productHighlightRepository.save(productHighlight);

    long activeCount = productHighlightRepository.countByCompanyIdAndApplicationIdAndIsActive(companyId, applicationId, true);

    if (activeCount < 1) {
    long deletedProxy = proxyRepository.deleteByCompanyIdAndApplicationId(companyId, applicationId);
    if (deletedProxy > 0) {
    platformClient.application(applicationId).partner.removeProxyPath(
    extensionProperties.getApiKey(), config.getProxyAttachPath()
    );
    }
    platformClient.application(applicationId).content.deleteAllInjectableTags();
    }
    } else {
    throw new RuntimeException("Invalid Item Code" + itemCode);
    }

    return ResponseEntity.status(HttpStatus.NO_CONTENT).body("Success");
    }
    }

    In this ScriptController class we have created APIs which will be called when activating or deactivating toggle button on the product list page.

    This APIs will create and destroy the proxy by calling to addProxyPath and removeProxyPath methods of the platformClient respectively.

    We will use this proxy to call our Extension endpoints from the store front website which will prevent the CORS error thrown by the browser.

    tip

    For API documentation on addProxyPath and removeProxyPath, please refer to these links.

    tip

    To learn more about how proxy URLs work, visit this link.

  • Also, add the following endpoints to the Endpoints object in /app/src/service/endpoint.service.js file.

    INJECTABLE_TAG(application_id, item_code) {
    return urlJoin(envVars.EXAMPLE_MAIN_URL, `api/v1.0/${application_id}/tag/${item_code}`);
    }
  • Also, add the following methods to the MainService object in /app/src/service/main-service.js file.

    // script inject
    addInjectableTag(application_id, item_code) {
    return axios.post(URLS.INJECTABLE_TAG(application_id, item_code));
    },
    deleteInjectableTag(application_id, item_code) {
    return axios.delete(URLS.INJECTABLE_TAG(application_id, item_code));
    }

Now we’ll develop APIs for the application. Both the following APIs will be called from the Product Description Page of the store front. One of the API will return the highlights of the product and the other one will return the boolean value on whether to show Price Drop tag or not.

Before that we will need one MongoDB schema to store the data about whether product’s price has dropped in last 2 days or not.

  • Create /com/fynd/example/java/db/PriceDrop.java class and add the following code

    package com.fynd.example.java.db;

    import com.fasterxml.jackson.annotation.JsonProperty;
    import org.springframework.data.mongodb.core.mapping.Document;
    import org.springframework.data.mongodb.core.mapping.Field;
    import org.springframework.data.mongodb.core.mapping.FieldType;
    import lombok.*;
    import java.util.Date;

    @Getter
    @Setter
    @Document(collection = "price-drop")
    @NoArgsConstructor
    @AllArgsConstructor
    public class PriceDrop {

    @JsonProperty("product_slug")
    private String productSlug;

    @Field(targetType = FieldType.DATE_TIME)
    private Date updatedAt;
    }
  • Also create /com/fynd/example/java/db/interfaces/PriceDropRepository.java interface and add following code.

    package com.fynd.example.java.db.interfaces;

    import com.fynd.example.java.db.PriceDrop;
    import org.springframework.data.mongodb.repository.MongoRepository;
    import org.springframework.stereotype.Repository;

    import java.util.Optional;

    @Repository
    public interface PriceDropRepository extends MongoRepository<PriceDrop, String> {

    Optional<PriceDrop> findOneByProductSlug(String productSlug);
    }
  • In the /com/fynd/example/java/controller package, create a new class called AppController and add the following code to the file:

    package com.fynd.example.java.controller;

    import com.fynd.example.java.db.PriceDrop;
    import com.fynd.example.java.db.ProductHighlight;
    import com.fynd.example.java.db.interfaces.PriceDropRepository;
    import com.fynd.example.java.db.interfaces.ProductHighlightRepository;
    import com.fynd.extension.controllers.BaseApplicationController;
    import com.fynd.extension.model.Application;
    import jakarta.servlet.http.HttpServletRequest;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Collections;
    import java.util.Optional;

    @RestController
    @RequestMapping("/app/proxy")
    @Slf4j
    public class AppController extends BaseApplicationController {

    @Autowired
    ProductHighlightRepository productHighlightRepository;

    @Autowired
    PriceDropRepository priceDropRepository;

    @GetMapping("/highlight")
    public ResponseEntity getHighlights(
    @RequestParam(value = "slug") String slug,
    HttpServletRequest request
    ) {
    try {
    Application application = (Application) request.getAttribute("application");
    String applicationId = application.getID();

    Optional<ProductHighlight> data
    = productHighlightRepository.findOneByApplicationIdAndProductSlug(applicationId, slug);

    if (data.isPresent()) {
    return ResponseEntity.status(HttpStatus.OK).body(data.get());
    } else {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Not Found");
    }

    } catch (Exception e) {
    System.out.println(e.getMessage());
    throw new RuntimeException(e);
    }
    }

    @GetMapping("/price-drop")
    public ResponseEntity getPriceDrop(
    @RequestParam(value = "slug") String slug,
    HttpServletRequest request
    ) {
    try {
    Application application = (Application) request.getAttribute("application");
    String applicationId = application.getID();

    Optional<PriceDrop> data = priceDropRepository.findOneByProductSlug(slug);
    if (data.isPresent()) {
    Optional<ProductHighlight> productHighlightOptional
    = productHighlightRepository.findOneByApplicationIdAndProductSlug(applicationId, slug);

    if (productHighlightOptional.isPresent() && productHighlightOptional.get().getIsActive()) {
    return ResponseEntity.status(HttpStatus.OK).body(
    Collections.singletonMap(
    "showPriceDrop",
    productHighlightOptional.get().getProduct().getEnablePriceDrop()
    )
    );
    } else {
    return ResponseEntity.status(HttpStatus.OK).body(Collections.singletonMap("showPriceDrop", false));
    }
    } else {
    return ResponseEntity.status(HttpStatus.OK).body(Collections.singletonMap("showPriceDrop", false));
    }
    } catch (Exception e) {
    System.out.println(e.getMessage());
    throw new RuntimeException(e);
    }
    }
    }

Step 5: Restart the extension server and relaunch the Extension

caution

When the Ngrok tunnel is restarted, the extension's local development base URL also changes. Consequently, any existing proxy for a sales channel won't function properly. To address this, it's advisable to remove the old proxy associated with the previous Ngrok tunnel URL and add the new Ngrok tunnel URL as a proxy.

The simplest approach is to uninstall the extension from the development company and then reinstall it.

As shown on below image, when clicking on Toggle button, API call is made to Extension backend. Which will inject the script into store front theme.

click on toggle button

But highlights won’t be visible on the product description page of the product for which we have activated highlights. To show the highlights user will have to configure extension binding in their theme.

  1. Visit platform.fynd.com

  2. Select the sales channel

  3. Appearance > Themes

  4. Edit the current active theme

    edit current active theme

  5. Select Product Description from top menu and select any product select product description from dropdown

  6. Select Page tab of Sections and Scroll down for Extension Positions go to page section

  7. Add Product Highlight Extension at Below Price Component position and Below Product Info position as shown in below image add product highlights and price drop wrapper

  8. Click on Save

    Now Visit PDP page of the product for which you have activated highlights. You should be able to get see the highlight below Product Info.

    product highlight in sales channel