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.
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
- Visit Fynd Partners.
- Click on Manage and Select your organization.
- Click on Extensions from the left Menu.
- Select your Extension.
- Click on three dots and then select Bindings as shown in the below image.
- Click on Add Template and fill in the data as shown in the below image.
- Repeat Step 6 for the Price Drop Tag also.
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 commandmkdir bindings
cd bindings -
Create Vue project
vue create .
noteChoose 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 codemodule.exports = {
pages: {
index: {
// entry for the page
entry: process.env.NODE_ENV == "development" ? 'src/dev.js' : 'src/main.js'
},
},
devServer: {
disableHostCheck: true
}
} -
Install
axios
andurl-join
package inside/bindings/
directory using the following commandsnpm 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
andjsconfig.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 calleddev.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 calledmain.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 calledHighlights.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} }
);
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> -
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 calledPriceDrop.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} }
);
if (data && data.showPriceDrop) {
this.showPriceDrop = true;
}
}
}
</script>
<style scoped>
img {
height: 48px;
width: auto;
}
</style> -
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
filepackage 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 codepackage 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
directorypackage 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 schemapackage 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 calledScriptController.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
andremoveProxyPath
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.
tipFor API documentation on addProxyPath and removeProxyPath , please refer to these links.
tipTo 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 codepackage 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 calledAppController
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
When the 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 tunnel URL and add the new 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.
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.
- Visit platform.fynd.com
- Select the sales channel
- Appearance > Themes
- Edit the current active theme
-
Select Product Description from top menu and select any product
-
Select Page tab of Sections and Scroll down for Extension Positions
-
Add Product Highlight Extension at Below Price Component position and Below Product Info position as shown in below image
-
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.