Skip to content

feat: enhance embed functionality with JSONSchema and version handling #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-houses-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackla/widget-utils": patch
---

Add support for versioning
48 changes: 21 additions & 27 deletions src/embed/embed.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
/* eslint-disable no-var */
import { embed } from "."
import { embed, JSONSchema } from "."
import fetchMock from "jest-fetch-mock"
import { getWidgetV2EmbedCode } from "./v2"
import { getWidgetV3EmbedCode } from "./v3"
import { generateDataHTMLStringByParams } from "./embed.params"

fetchMock.enableMocks()

const REQUEST_URL = "https://widget-data.stackla.com/widgets/123/version"
const STAGING_REQUEST_URL = "https://widget-data.teaser.stackla.com/widgets/123/version"
const REQUEST_URL = "https://widget-data.stackla.com/widgets/123/version/"
const STAGING_REQUEST_URL = "https://widget-data.teaser.stackla.com/widgets/123/version/"

const V2Request: JSONSchema = {
widgetVersion: 2,
scriptVersion: 1
}

const V3Request: JSONSchema = {
widgetVersion: 3,
scriptVersion: 1
}

describe("load embed code", () => {
beforeEach(() => {
Expand All @@ -17,7 +27,7 @@ describe("load embed code", () => {

it("should return the correct embed code for v2", async () => {
fetchMock.mockIf(REQUEST_URL, async () => {
return JSON.stringify({ version: 2 })
return JSON.stringify(V2Request)
})

const createdDiv = document.createElement("div")
Expand All @@ -37,7 +47,7 @@ describe("load embed code", () => {

it("should test staging for v2", async () => {
fetchMock.mockIf(STAGING_REQUEST_URL, async () => {
return JSON.stringify({ version: 2 })
return JSON.stringify(V2Request)
})

const createdDiv = document.createElement("div")
Expand All @@ -56,7 +66,7 @@ describe("load embed code", () => {

it("should test production for v2", async () => {
fetchMock.mockIf(REQUEST_URL, async () => {
return JSON.stringify({ version: 2 })
return JSON.stringify(V2Request)
})

const createdDiv = document.createElement("div")
Expand All @@ -75,7 +85,7 @@ describe("load embed code", () => {

it("should return the correct embed code for v3", async () => {
fetchMock.mockIf(REQUEST_URL, async () => {
return JSON.stringify({ version: 3 })
return JSON.stringify(V3Request)
})

const createdDiv = document.createElement("div")
Expand Down Expand Up @@ -114,36 +124,21 @@ describe("load embed code", () => {
}
})

it("should skip the fetch call if the version is provided", async () => {
const createdDiv = document.createElement("div")
await embed({
widgetId: "123",
root: createdDiv,
version: 3,
dataProperties: {
foo: "bar",
baz: 123
},
environment: "production"
})

expect(fetchMock).not.toHaveBeenCalled()
expect(createdDiv.innerHTML).toContain(getWidgetV3EmbedCode({ foo: "bar", baz: 123, wid: "123" }))
expect(createdDiv.innerHTML).toContain('<div id="ugc-widget" data-foo="bar" data-baz="123" data-wid="123"></div>')
})

it("should test param string method", async () => {
const params = generateDataHTMLStringByParams({ foo: "bar", baz: 123, wid: "123" })

expect(params).toBe(' data-foo="bar" data-baz="123" data-wid="123"')
})

it("should deal with malicious payloads", async () => {
fetchMock.mockIf(REQUEST_URL, async () => {
return JSON.stringify(V3Request)
})

const createdDiv = document.createElement("div")
await embed({
widgetId: "123",
root: createdDiv,
version: 3,
dataProperties: {
foo: "bar",
baz: 123,
Expand All @@ -152,7 +147,6 @@ describe("load embed code", () => {
environment: "production"
})

expect(fetchMock).not.toHaveBeenCalled()
expect(createdDiv.innerHTML).toContain(
`<div id="ugc-widget" data-foo="bar" data-baz="123" data-%3e%3cimg%20src%3d%22x%22%20onerror%3d%22alert(1)%22%3e="%22%3E%3Cimg%20src%3D%22x%22%20onerror%3D%22alert(1)%22%3E" data-wid="123"></div>`
)
Expand Down
22 changes: 10 additions & 12 deletions src/embed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@
import { getWidgetV3EmbedCode, invokeV3Javascript } from "./v3"

export type Environment = "staging" | "production"
type Generation = 2 | 3

interface EmbedOptions<T> {
widgetId: string
root: T
environment: Environment
version?: Generation
dataProperties: Record<string, string | number | boolean>
}

interface JSONSchema {
version: number
export interface JSONSchema {
widgetVersion: number
scriptVersion: number
}

export function getWidgetDataUrl(env: Environment) {
Expand All @@ -41,25 +40,24 @@
}

function getRequestUrl(widgetId: string, environment: Environment) {
return `${getWidgetDataUrl(environment)}/widgets/${widgetId}/version`
return `${getWidgetDataUrl(environment)}/widgets/${widgetId}/version/`
}

async function retrieveWidgetVersionFromServer(widgetId: string, environment: Environment): Promise<number> {
async function retrieveWidgetMetaFromServer(widgetId: string, environment: Environment): Promise<JSONSchema> {
const response = await fetch(getRequestUrl(widgetId, environment))
const json: JSONSchema = await response.json()

return json.version
return response.json()
}

function injectHTML(root: HTMLElement | ShadowRoot, html: string) {
root.appendChild(document.createRange().createContextualFragment(html))
}

export async function embed<T extends ShadowRoot | HTMLElement>(options: EmbedOptions<T>) {
const { environment, widgetId, root, version, dataProperties } = options
const { environment, widgetId, root, dataProperties } = options

try {
const widgetVersion = version ?? (await retrieveWidgetVersionFromServer(widgetId, environment))
const widgetMeta = await retrieveWidgetMetaFromServer(widgetId, environment)
const { widgetVersion, scriptVersion } = widgetMeta
switch (widgetVersion) {
case 2:
window.stackWidgetDomain = getLegacyWidgetDomain(environment)
Expand All @@ -70,12 +68,12 @@
case 3:
dataProperties["wid"] = widgetId
injectHTML(root, getWidgetV3EmbedCode(dataProperties))
await invokeV3Javascript(environment)
await invokeV3Javascript(environment, scriptVersion)
break
default:
throw new Error(`No widget code accessible with version ${widgetVersion}`)
}
} catch (error) {
console.error(`Failed to embed widget. ${error}`)

Check warning on line 77 in src/embed/index.ts

View workflow job for this annotation

GitHub Actions / Build and Test

Unexpected console statement
}
}
4 changes: 2 additions & 2 deletions src/embed/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const getWidgetV3EmbedCode = (data: Record<string, string | boolean | number>) =
return `<div id="ugc-widget"${dataParams}></div>`
}

const invokeV3Javascript = async (environment: Environment) => {
const widget = await import(`${getUrlByEnv(environment)}/core.esm.js`)
const invokeV3Javascript = async (environment: Environment, scriptVersion: number) => {
const widget = await import(`${getUrlByEnv(environment)}/v${scriptVersion}/core.esm.js`)
widget.init()
}

Expand Down
Loading