From 5f18b67c80f1f22e6a3c9f441e28a6a712e25854 Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Tue, 20 Jan 2026 17:21:26 -0800 Subject: [PATCH 1/9] added support for granular tempalte editor --- playground/Playground.tsx | 145 +++++++++++---- src/calls/embedTemplateEditorPage.ts | 254 +++++++++++++++++++++++++++ src/index.ts | 9 + 3 files changed, 370 insertions(+), 38 deletions(-) create mode 100644 src/calls/embedTemplateEditorPage.ts diff --git a/playground/Playground.tsx b/playground/Playground.tsx index 76d3cef..2004713 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -24,8 +24,10 @@ import {useEffect, useRef, useState} from "react"; interface Settings { apiKey: string; templateId: string | null; + embedType: "fullEmbed" | "templateEditor"; permissions: string[]; features: Required; + editorFeatures: badge.TemplateEditorFeatures; sdkPath: string; googleFont: string; primaryColor: string; @@ -54,7 +56,10 @@ export function Playground() { const [sdkCall, setSdkCall] = useState<{ sdkOptions: badge.SdkOptions; - embedTemplatePageOptions: badge.EmbedTemplatePageOptions; + options: + | badge.EmbedTemplatePageOptions + | badge.EmbedTemplateEditorPageOptions; + functionName: string; }>(); function launchEmbed() { @@ -95,31 +100,50 @@ export function Playground() { const primaryColor = settings.primaryColor || undefined; const neutralColor = settings.neutralColor || undefined; - const embedTemplatePageOptions: badge.EmbedTemplatePageOptions = { - templateId, - features: settings.features, - fonts: googleFont - ? [ - { - cssSrc: `https://fonts.googleapis.com/css2?family=${googleFont.replace(/ /g, "+")}&display=swap`, - }, - ] - : undefined, - appearance: { - fontFamily: googleFont, - colors: { - primary: primaryColor, - neutral: neutralColor, - }, + const fonts = googleFont + ? [ + { + cssSrc: `https://fonts.googleapis.com/css2?family=${googleFont.replace(/ /g, "+")}&display=swap`, + }, + ] + : undefined; + + const appearance = { + fontFamily: googleFont, + colors: { + primary: primaryColor, + neutral: neutralColor, }, }; - badge.embedTemplatePage(sdk, element, embedTemplatePageOptions); - - setSdkCall({ - sdkOptions, - embedTemplatePageOptions, - }); + if (settings.embedType === "templateEditor") { + const options: badge.EmbedTemplateEditorPageOptions = { + templateId, + features: + settings.editorFeatures ?? badge.TEMPLATE_EDITOR_FEATURES_DEFAULT, + fonts, + appearance, + }; + badge.embedTemplateEditorPage(sdk, element, options); + setSdkCall({ + sdkOptions, + options, + functionName: "embedTemplateEditorPage", + }); + } else { + const options: badge.EmbedTemplatePageOptions = { + templateId, + features: settings.features, + fonts, + appearance, + }; + badge.embedTemplatePage(sdk, element, options); + setSdkCall({ + sdkOptions, + options, + functionName: "embedTemplatePage", + }); + } }) .catch(alert); } @@ -193,27 +217,61 @@ export function Playground() { settingChanged("templateId", value); }} /> - { - settingChanged("permissions", value); + settingChanged("embedType", value as Settings["embedType"]); }} /> value) - .map(([key]) => key)} + label="Permissions" + data={ + settings.embedType === "fullEmbed" + ? ALL_PERMISSIONS + : ALL_PERMISSIONS_TEMPLATE_EDITOR + } + value={settings.permissions} onChange={(value) => { - settingChanged("features", { - ...ALL_FEATURES_DISABLED, - ...Object.fromEntries(value.map((flag) => [flag, true])), - }); + settingChanged("permissions", value); }} /> + {settings.embedType === "fullEmbed" ? ( + value) + .map(([key]) => key)} + onChange={(value) => { + settingChanged("features", { + ...ALL_FEATURES_DISABLED, + ...Object.fromEntries(value.map((flag) => [flag, true])), + }); + }} + /> + ) : ( + value) + .map(([key]) => key)} + onChange={(value) => { + const editorFeatures: badge.TemplateEditorFeatures = + Object.fromEntries(value.map((flag) => [flag, true])); + settingChanged("editorFeatures", editorFeatures); + }} + /> + )} {` const sdk = badge.makeSdk(${JSON.stringify(sdkCall.sdkOptions, null, 2)}); -badge.embedTemplatePage(sdk, element, ${JSON.stringify(sdkCall.embedTemplatePageOptions, null, 2)});`} +badge.${sdkCall.functionName}(sdk, element, ${JSON.stringify(sdkCall.options, null, 2)});`} )} @@ -295,6 +353,7 @@ badge.embedTemplatePage(sdk, element, ${JSON.stringify(sdkCall.embedTemplatePage const DEFAULT_SETTINGS: Settings = { apiKey: "", templateId: null, + embedType: "fullEmbed", permissions: [ "workspace:read", "user:write", @@ -307,6 +366,7 @@ const DEFAULT_SETTINGS: Settings = { campaigns: true, campaignEditor: true, }, + editorFeatures: {...badge.TEMPLATE_EDITOR_FEATURES_DEFAULT}, sdkPath: "", googleFont: "", primaryColor: "", @@ -333,3 +393,12 @@ const ALL_PERMISSIONS = [ "user:read", "user:write", ]; + +const ALL_PERMISSIONS_TEMPLATE_EDITOR = [ + "workspace:read", + "workspace:write", + "template:read", + "template:write", + "user:read", + "user:write", +]; \ No newline at end of file diff --git a/src/calls/embedTemplateEditorPage.ts b/src/calls/embedTemplateEditorPage.ts new file mode 100644 index 0000000..d53a4a0 --- /dev/null +++ b/src/calls/embedTemplateEditorPage.ts @@ -0,0 +1,254 @@ +import * as z from "@zod/mini"; +import type {BadgeSdk} from "../sdk.ts"; + +export interface EmbedTemplateEditorPageOptions { + /** + * id of the template to show + */ + templateId: string; + features?: TemplateEditorFeatures | undefined; + fonts?: FontSource[] | undefined; + appearance?: AppearanceConfig | undefined; +} + +/** + * requires template:read + */ +export function embedTemplateEditorPage( + sdk: BadgeSdk, + element: HTMLElement, + options: EmbedTemplateEditorPageOptions, +): void { + const {templateId, features, fonts, appearance} = options; + const {workspaceHandle, permissions} = parseTokenPayload(sdk.token); + + validatePermissions(permissions, features ?? {}); + + const iframe = createEmbedIframe({ + token: sdk.token, + path: sdk.path, + workspaceHandle, + templateId, + config: { + features, + fonts, + appearance, + }, + }); + + element.replaceChildren(iframe); +} + +interface CreateIframeOptions { + token: string; + path: string; + workspaceHandle: string; + templateId: string; + config: TemplateEditorEmbedConfig; +} + +interface TemplateEditorEmbedConfig { + features?: TemplateEditorFeatures | undefined; + fonts?: FontSource[] | undefined; + appearance?: AppearanceConfig | undefined; +} + +export type FontSource = CssFontSource | CustomFontSource; + +export interface CssFontSource { + cssSrc: string; +} + +export interface CustomFontSource { + family: string; + src: string; + weight?: + | "100" + | "200" + | "300" + | "400" + | "500" + | "600" + | "700" + | "800" + | "900"; + style?: "normal" | "italic" | "oblique"; + unicodeRange?: string; +} + +export interface AppearanceConfig { + fontFamily?: string | undefined; + colors?: { + primary?: string | undefined; + neutral?: string | undefined; + }; +} + +function createEmbedIframe(options: CreateIframeOptions): HTMLIFrameElement { + const {path, token, workspaceHandle, templateId, config} = options; + + const iframe = document.createElement("iframe"); + + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "none"; + + const iframeUrl = new URL(path); + + iframeUrl.searchParams.set("token", token); + iframeUrl.searchParams.set("config", JSON.stringify(config)); + + iframeUrl.hash = `/${workspaceHandle}/templates/${templateId}/edit`; + + iframe.src = iframeUrl.toString(); + + return iframe; +} + +function parseTokenPayload(token: string): SdkTokenPayload { + const tokenPayloadResult = sdkTokenPayloadSchema.safeParse( + JSON.parse(atob(token.split(".")[1] ?? "")), + ); + + if (!tokenPayloadResult.success) { + throw new Error("Invalid token payload"); + } + + return tokenPayloadResult.data; +} + +function validatePermissions( + permissions: SdkPermission[], + features: TemplateEditorFeatures, +): void { + const hasPermission = (validPermissionSets: SdkPermission[][]) => { + return validPermissionSets.every((permissionSet) => + permissionSet.some((permission) => permissions.includes(permission)), + ); + }; + + if (!hasPermission(REQUIRED_PERMISSIONS)) { + throw new Error( + REQUIRED_PERMISSIONS.map((set) => set.join(" or ")).join(", ") + + " permissions are required", + ); + } + + if ( + features && + typeof features === "object" && + Object.keys(features).length === 0 + ) { + throw new Error( + "features cannot be an empty object. Omit the parameter or specify at least one feature.", + ); + } +} + +type SdkPermission = z.infer; +const sdkPermissionSchema = z.enum([ + "workspace:read", + "workspace:write", + "template:read", + "template:write", + "user:read", + "user:write", +]); + +type SdkTokenPayload = z.infer; +const sdkTokenPayloadSchema = z.object({ + workspaceHandle: z.string(), + permissions: z.array(sdkPermissionSchema), +}); + +const REQUIRED_PERMISSIONS: SdkPermission[][] = [ + ["workspace:read", "workspace:write"], + ["user:read", "user:write"], + ["template:write", "template:read"], +]; + +export interface TemplateEditorFeatures { + logo?: boolean; + icon?: boolean; + heading?: boolean; + colors?: boolean; + coverImage?: boolean; + headerField?: boolean; + secondaryFields?: boolean; + barcodeType?: boolean; + barcodeData?: boolean; + barcodeText?: boolean; + description?: boolean; + backFields?: boolean; + locationMessages?: boolean; + appLinks?: boolean; + expiry?: boolean; +} + +export const TEMPLATE_EDITOR_FEATURES_DEFAULT = { + logo: true, + icon: true, + heading: true, + colors: true, + coverImage: true, + headerField: true, + secondaryFields: true, + barcodeType: false, + barcodeData: false, + barcodeText: false, + description: false, + backFields: true, + locationMessages: true, + appLinks: true, + expiry: false, +} satisfies Record; + +export const TEMPLATE_EDITOR_FEATURES_ALL: Record< + keyof TemplateEditorFeatures, + true +> = { + logo: true, + icon: true, + heading: true, + colors: true, + coverImage: true, + headerField: true, + secondaryFields: true, + barcodeType: true, + barcodeData: true, + barcodeText: true, + description: true, + backFields: true, + locationMessages: true, + appLinks: true, + expiry: true, +}; + +export const TEMPLATE_EDITOR_FEATURES_NONE: Record< + keyof TemplateEditorFeatures, + false +> = { + logo: false, + icon: false, + heading: false, + colors: false, + coverImage: false, + headerField: false, + secondaryFields: false, + barcodeType: false, + barcodeData: false, + barcodeText: false, + description: false, + backFields: false, + locationMessages: false, + appLinks: false, + expiry: false, +}; + +export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< + Record +> = { + barcodeType: true, + barcodeData: true, + barcodeText: true, +}; diff --git a/src/index.ts b/src/index.ts index 1841cca..742273e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,4 +5,13 @@ export { type EmbedTemplatePageOptions, type TemplateEmbedFeatures, } from "./calls/embedTemplatePage.ts"; +export { + embedTemplateEditorPage, + type EmbedTemplateEditorPageOptions, + type TemplateEditorFeatures, + TEMPLATE_EDITOR_FEATURES_DEFAULT, + TEMPLATE_EDITOR_FEATURES_ALL, + TEMPLATE_EDITOR_FEATURES_NONE, + TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE, +} from "./calls/embedTemplateEditorPage.ts"; export {makeSdk, type SdkOptions} from "./calls/makeSdk.ts"; From c761006036b3cb04455dd921f6d62baf830ba34e Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Tue, 20 Jan 2026 17:25:22 -0800 Subject: [PATCH 2/9] lint --- playground/Playground.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/Playground.tsx b/playground/Playground.tsx index 2004713..72fb23c 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -401,4 +401,4 @@ const ALL_PERMISSIONS_TEMPLATE_EDITOR = [ "template:write", "user:read", "user:write", -]; \ No newline at end of file +]; From 52356bd48d9ed29f7cfe7c2acf603a8c46b65159 Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Wed, 21 Jan 2026 11:54:02 -0800 Subject: [PATCH 3/9] extracted shared logic into helpers and utils --- src/calls/embedTemplateEditorPage.ts | 140 +++------------------------ src/calls/embedTemplatePage.ts | 139 ++++---------------------- src/helpers/appearance.ts | 30 ++++++ src/helpers/permissions.ts | 36 +++++++ src/helpers/token.ts | 20 ++++ src/utils/iframe.ts | 32 ++++++ 6 files changed, 153 insertions(+), 244 deletions(-) create mode 100644 src/helpers/appearance.ts create mode 100644 src/helpers/permissions.ts create mode 100644 src/helpers/token.ts create mode 100644 src/utils/iframe.ts diff --git a/src/calls/embedTemplateEditorPage.ts b/src/calls/embedTemplateEditorPage.ts index d53a4a0..d80a428 100644 --- a/src/calls/embedTemplateEditorPage.ts +++ b/src/calls/embedTemplateEditorPage.ts @@ -1,5 +1,10 @@ -import * as z from "@zod/mini"; import type {BadgeSdk} from "../sdk.ts"; +import type {AppearanceConfig} from "../helpers/appearance.ts"; +import type {FontSource} from "../helpers/appearance.ts"; +import {createEmbedIframe} from "../utils/iframe.ts"; +import {validatePermissions} from "../helpers/permissions.ts"; +import {parseTokenPayload} from "../helpers/token.ts"; +import {type SdkPermission} from "../helpers/permissions.ts"; export interface EmbedTemplateEditorPageOptions { /** @@ -17,150 +22,35 @@ export interface EmbedTemplateEditorPageOptions { export function embedTemplateEditorPage( sdk: BadgeSdk, element: HTMLElement, - options: EmbedTemplateEditorPageOptions, + {templateId, features, fonts, appearance}: EmbedTemplateEditorPageOptions, ): void { - const {templateId, features, fonts, appearance} = options; const {workspaceHandle, permissions} = parseTokenPayload(sdk.token); - validatePermissions(permissions, features ?? {}); + validatePermissions(permissions, REQUIRED_PERMISSIONS); + validateFeatures(features ?? {}); const iframe = createEmbedIframe({ token: sdk.token, path: sdk.path, - workspaceHandle, - templateId, - config: { - features, - fonts, - appearance, - }, + pathHash: `/${workspaceHandle}/templates/${templateId}/edit`, + config: {features, fonts, appearance}, }); element.replaceChildren(iframe); } -interface CreateIframeOptions { - token: string; - path: string; - workspaceHandle: string; - templateId: string; - config: TemplateEditorEmbedConfig; -} - -interface TemplateEditorEmbedConfig { +export interface TemplateEditorConfig { features?: TemplateEditorFeatures | undefined; fonts?: FontSource[] | undefined; appearance?: AppearanceConfig | undefined; } -export type FontSource = CssFontSource | CustomFontSource; - -export interface CssFontSource { - cssSrc: string; -} - -export interface CustomFontSource { - family: string; - src: string; - weight?: - | "100" - | "200" - | "300" - | "400" - | "500" - | "600" - | "700" - | "800" - | "900"; - style?: "normal" | "italic" | "oblique"; - unicodeRange?: string; -} - -export interface AppearanceConfig { - fontFamily?: string | undefined; - colors?: { - primary?: string | undefined; - neutral?: string | undefined; - }; -} - -function createEmbedIframe(options: CreateIframeOptions): HTMLIFrameElement { - const {path, token, workspaceHandle, templateId, config} = options; - - const iframe = document.createElement("iframe"); - - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.border = "none"; - - const iframeUrl = new URL(path); - - iframeUrl.searchParams.set("token", token); - iframeUrl.searchParams.set("config", JSON.stringify(config)); - - iframeUrl.hash = `/${workspaceHandle}/templates/${templateId}/edit`; - - iframe.src = iframeUrl.toString(); - - return iframe; -} - -function parseTokenPayload(token: string): SdkTokenPayload { - const tokenPayloadResult = sdkTokenPayloadSchema.safeParse( - JSON.parse(atob(token.split(".")[1] ?? "")), - ); - - if (!tokenPayloadResult.success) { - throw new Error("Invalid token payload"); +function validateFeatures(features: TemplateEditorFeatures): void { + if (features && Object.keys(features).length === 0) { + throw new Error("please specify at least one template editor feature."); } - - return tokenPayloadResult.data; } -function validatePermissions( - permissions: SdkPermission[], - features: TemplateEditorFeatures, -): void { - const hasPermission = (validPermissionSets: SdkPermission[][]) => { - return validPermissionSets.every((permissionSet) => - permissionSet.some((permission) => permissions.includes(permission)), - ); - }; - - if (!hasPermission(REQUIRED_PERMISSIONS)) { - throw new Error( - REQUIRED_PERMISSIONS.map((set) => set.join(" or ")).join(", ") + - " permissions are required", - ); - } - - if ( - features && - typeof features === "object" && - Object.keys(features).length === 0 - ) { - throw new Error( - "features cannot be an empty object. Omit the parameter or specify at least one feature.", - ); - } -} - -type SdkPermission = z.infer; -const sdkPermissionSchema = z.enum([ - "workspace:read", - "workspace:write", - "template:read", - "template:write", - "user:read", - "user:write", -]); - -type SdkTokenPayload = z.infer; -const sdkTokenPayloadSchema = z.object({ - workspaceHandle: z.string(), - permissions: z.array(sdkPermissionSchema), -}); - const REQUIRED_PERMISSIONS: SdkPermission[][] = [ ["workspace:read", "workspace:write"], ["user:read", "user:write"], diff --git a/src/calls/embedTemplatePage.ts b/src/calls/embedTemplatePage.ts index 7c307ec..8bc9e36 100644 --- a/src/calls/embedTemplatePage.ts +++ b/src/calls/embedTemplatePage.ts @@ -1,5 +1,13 @@ -import * as z from "@zod/mini"; import type {BadgeSdk} from "../sdk.ts"; +import type {FontSource} from "../helpers/appearance.ts"; +import type {AppearanceConfig} from "../helpers/appearance.ts"; +import {createEmbedIframe} from "../utils/iframe.ts"; +import {parseTokenPayload} from "../helpers/token.ts"; +import { + hasPermission, + validatePermissions, + type SdkPermission, +} from "../helpers/permissions.ts"; export interface EmbedTemplatePageOptions { /** @@ -17,73 +25,29 @@ export interface EmbedTemplatePageOptions { export function embedTemplatePage( sdk: BadgeSdk, element: HTMLElement, - options: EmbedTemplatePageOptions, + {templateId, features, fonts, appearance}: EmbedTemplatePageOptions, ): void { - const {templateId, features, fonts, appearance} = options; const {workspaceHandle, permissions} = parseTokenPayload(sdk.token); - validatePermissions(permissions, features ?? {}); + validatePermissions(permissions, REQUIRED_PERMISSIONS); + validatePermissionsByFeatures(permissions, features ?? {}); const iframe = createEmbedIframe({ token: sdk.token, path: sdk.path, - workspaceHandle, - templateId, - config: { - features, - fonts, - appearance, - }, + pathHash: `/${workspaceHandle}/templates/${templateId}/overview`, + config: {features, fonts, appearance}, }); element.replaceChildren(iframe); } -interface CreateIframeOptions { - token: string; - path: string; - workspaceHandle: string; - templateId: string; - config: TemplateEmbedConfig; -} - -interface TemplateEmbedConfig { +export interface TemplateEmbedConfig { features?: TemplateEmbedFeatures | undefined; fonts?: FontSource[] | undefined; appearance?: AppearanceConfig | undefined; } -export type FontSource = CssFontSource | CustomFontSource; - -export interface CssFontSource { - cssSrc: string; -} - -export interface CustomFontSource { - family: string; - src: string; - weight?: - | "100" - | "200" - | "300" - | "400" - | "500" - | "600" - | "700" - | "800" - | "900"; - style?: "normal" | "italic" | "oblique"; - unicodeRange?: string; -} - -export interface AppearanceConfig { - fontFamily?: string | undefined; - colors?: { - primary?: string | undefined; - neutral?: string | undefined; - }; -} - export interface TemplateEmbedFeatures { passList?: boolean; templateEditor?: boolean; @@ -91,56 +55,10 @@ export interface TemplateEmbedFeatures { campaignEditor?: boolean; } -function createEmbedIframe(options: CreateIframeOptions): HTMLIFrameElement { - const {path, token, workspaceHandle, templateId, config} = options; - - const iframe = document.createElement("iframe"); - - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.border = "none"; - - const iframeUrl = new URL(path); - - iframeUrl.searchParams.set("token", token); - iframeUrl.searchParams.set("config", JSON.stringify(config)); - - iframeUrl.hash = `/${workspaceHandle}/templates/${templateId}/overview`; - - iframe.src = iframeUrl.toString(); - - return iframe; -} - -function parseTokenPayload(token: string): SdkTokenPayload { - const tokenPayloadResult = sdkTokenPayloadSchema.safeParse( - JSON.parse(atob(token.split(".")[1] ?? "")), - ); - - if (!tokenPayloadResult.success) { - throw new Error("Invalid token payload"); - } - - return tokenPayloadResult.data; -} - -function validatePermissions( +function validatePermissionsByFeatures( permissions: SdkPermission[], features: TemplateEmbedFeatures, ): void { - const hasPermission = (validPermissionSets: SdkPermission[][]) => { - return validPermissionSets.every((permissionSet) => - permissionSet.some((permission) => permissions.includes(permission)), - ); - }; - - if (!hasPermission(REQUIRED_PERMISSIONS)) { - throw new Error( - REQUIRED_PERMISSIONS.map((set) => set.join(" or ")).join(", ") + - " permissions are required", - ); - } - const failedPermissions = Object.entries(features) .filter(([_, enabled]) => enabled) .map(([feature]) => ({ @@ -149,7 +67,10 @@ function validatePermissions( REQUIRED_PERMISSIONS_BY_FEATURE[feature as keyof TemplateEmbedFeatures], })) .filter(({permissions}) => permissions !== undefined) - .filter(({permissions}) => !hasPermission(permissions)); + .filter( + ({permissions: featurePermissions}) => + !hasPermission(permissions, featurePermissions), + ); if (failedPermissions.length > 0) { throw new Error( @@ -163,26 +84,6 @@ function validatePermissions( } } -type SdkPermission = z.infer; -const sdkPermissionSchema = z.enum([ - "workspace:read", - "workspace:write", - "template:read", - "template:write", - "pass:read", - "pass:write", - "campaign:read", - "campaign:write", - "user:read", - "user:write", -]); - -type SdkTokenPayload = z.infer; -const sdkTokenPayloadSchema = z.object({ - workspaceHandle: z.string(), - permissions: z.array(sdkPermissionSchema), -}); - const REQUIRED_PERMISSIONS: SdkPermission[][] = [ ["workspace:read", "workspace:write"], ["user:read", "user:write"], diff --git a/src/helpers/appearance.ts b/src/helpers/appearance.ts new file mode 100644 index 0000000..1b34782 --- /dev/null +++ b/src/helpers/appearance.ts @@ -0,0 +1,30 @@ +export interface AppearanceConfig { + fontFamily?: string | undefined; + colors?: { + primary?: string | undefined; + neutral?: string | undefined; + }; +} + +export type FontSource = CssFontSource | CustomFontSource; + +export interface CssFontSource { + cssSrc: string; +} + +export interface CustomFontSource { + family: string; + src: string; + weight?: + | "100" + | "200" + | "300" + | "400" + | "500" + | "600" + | "700" + | "800" + | "900"; + style?: "normal" | "italic" | "oblique"; + unicodeRange?: string; +} diff --git a/src/helpers/permissions.ts b/src/helpers/permissions.ts new file mode 100644 index 0000000..4bbe972 --- /dev/null +++ b/src/helpers/permissions.ts @@ -0,0 +1,36 @@ +import {z} from "@zod/mini"; + +export function hasPermission( + permissions: SdkPermission[], + validPermissionSets: SdkPermission[][], +): boolean { + return validPermissionSets.every((permissionSet) => + permissionSet.some((permission) => permissions.includes(permission)), + ); +} + +export function validatePermissions( + permissions: SdkPermission[], + validPermissionSets: SdkPermission[][], +): void { + if (!hasPermission(permissions, validPermissionSets)) { + throw new Error( + validPermissionSets.map((set) => set.join(" or ")).join(", ") + + " permissions are required", + ); + } +} + +export type SdkPermission = z.infer; +export const sdkPermissionSchema = z.enum([ + "workspace:read", + "workspace:write", + "template:read", + "template:write", + "pass:read", + "pass:write", + "campaign:read", + "campaign:write", + "user:read", + "user:write", +]); diff --git a/src/helpers/token.ts b/src/helpers/token.ts new file mode 100644 index 0000000..d771eb7 --- /dev/null +++ b/src/helpers/token.ts @@ -0,0 +1,20 @@ +import {z} from "@zod/mini"; +import {sdkPermissionSchema} from "./permissions"; + +export function parseTokenPayload(token: string): SdkTokenPayload { + const tokenPayloadResult = sdkTokenPayloadSchema.safeParse( + JSON.parse(atob(token.split(".")[1] ?? "")), + ); + + if (!tokenPayloadResult.success) { + throw new Error("Invalid token payload"); + } + + return tokenPayloadResult.data; +} + +export type SdkTokenPayload = z.infer; +const sdkTokenPayloadSchema = z.object({ + workspaceHandle: z.string(), + permissions: z.array(sdkPermissionSchema), +}); diff --git a/src/utils/iframe.ts b/src/utils/iframe.ts new file mode 100644 index 0000000..296f25c --- /dev/null +++ b/src/utils/iframe.ts @@ -0,0 +1,32 @@ +import type {TemplateEditorConfig} from "../calls/embedTemplateEditorPage"; +import type {TemplateEmbedConfig} from "../calls/embedTemplatePage"; + +interface CreateIframeOptions { + token: string; + path: string; + pathHash: string; + config: TemplateEmbedConfig | TemplateEditorConfig; +} + +export function createEmbedIframe( + options: CreateIframeOptions, +): HTMLIFrameElement { + const {path, token, pathHash, config} = options; + + const iframe = document.createElement("iframe"); + + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "none"; + + const iframeUrl = new URL(path); + + iframeUrl.searchParams.set("token", token); + iframeUrl.searchParams.set("config", JSON.stringify(config)); + + iframeUrl.hash = pathHash; + + iframe.src = iframeUrl.toString(); + + return iframe; +} From 4dc4cf8c0a5e87d619733e86f2c78885668ac2b4 Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Wed, 21 Jan 2026 11:59:01 -0800 Subject: [PATCH 4/9] lint --- src/calls/embedTemplateEditorPage.ts | 3 +-- src/calls/embedTemplatePage.ts | 3 +-- src/helpers/token.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/calls/embedTemplateEditorPage.ts b/src/calls/embedTemplateEditorPage.ts index d80a428..2c63a19 100644 --- a/src/calls/embedTemplateEditorPage.ts +++ b/src/calls/embedTemplateEditorPage.ts @@ -1,6 +1,5 @@ import type {BadgeSdk} from "../sdk.ts"; -import type {AppearanceConfig} from "../helpers/appearance.ts"; -import type {FontSource} from "../helpers/appearance.ts"; +import type {AppearanceConfig, FontSource} from "../helpers/appearance.ts"; import {createEmbedIframe} from "../utils/iframe.ts"; import {validatePermissions} from "../helpers/permissions.ts"; import {parseTokenPayload} from "../helpers/token.ts"; diff --git a/src/calls/embedTemplatePage.ts b/src/calls/embedTemplatePage.ts index 8bc9e36..28ada99 100644 --- a/src/calls/embedTemplatePage.ts +++ b/src/calls/embedTemplatePage.ts @@ -1,6 +1,5 @@ import type {BadgeSdk} from "../sdk.ts"; -import type {FontSource} from "../helpers/appearance.ts"; -import type {AppearanceConfig} from "../helpers/appearance.ts"; +import type {FontSource, AppearanceConfig} from "../helpers/appearance.ts"; import {createEmbedIframe} from "../utils/iframe.ts"; import {parseTokenPayload} from "../helpers/token.ts"; import { diff --git a/src/helpers/token.ts b/src/helpers/token.ts index d771eb7..10ce4e3 100644 --- a/src/helpers/token.ts +++ b/src/helpers/token.ts @@ -1,5 +1,5 @@ import {z} from "@zod/mini"; -import {sdkPermissionSchema} from "./permissions"; +import {sdkPermissionSchema} from "./permissions.ts"; export function parseTokenPayload(token: string): SdkTokenPayload { const tokenPayloadResult = sdkTokenPayloadSchema.safeParse( From cb7b48fb78a8a8e6e49ddfb535ffeb5caab4119f Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Thu, 22 Jan 2026 17:30:42 -0800 Subject: [PATCH 5/9] passing tempalte editor features with tempalte editor --- playground/Playground.tsx | 15 ++-- src/calls/embedTemplateEditorPage.ts | 115 ++++----------------------- src/calls/embedTemplatePage.ts | 14 +--- src/helpers/embedFeatures.ts | 92 +++++++++++++++++++++ src/index.ts | 8 +- src/utils/iframe.ts | 10 ++- 6 files changed, 128 insertions(+), 126 deletions(-) create mode 100644 src/helpers/embedFeatures.ts diff --git a/playground/Playground.tsx b/playground/Playground.tsx index 72fb23c..b736598 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -24,7 +24,7 @@ import {useEffect, useRef, useState} from "react"; interface Settings { apiKey: string; templateId: string | null; - embedType: "fullEmbed" | "templateEditor"; + embedType: "templateDetails" | "templateEditor"; permissions: string[]; features: Required; editorFeatures: badge.TemplateEditorFeatures; @@ -221,7 +221,7 @@ export function Playground() { label="Embed Type" defaultValue={settings.embedType} data={[ - {value: "fullEmbed", label: "Full Embed"}, + {value: "templateDetails", label: "Template Details"}, {value: "templateEditor", label: "Template Editor"}, ]} value={settings.embedType} @@ -232,16 +232,17 @@ export function Playground() { { settingChanged("permissions", value); }} /> - {settings.embedType === "fullEmbed" ? ( + {settings.embedType === "templateDetails" ? ( 0 + ? {...TEMPLATE_EDITOR_FEATURES_NONE, ...features} + : TEMPLATE_EDITOR_FEATURES_DEFAULT, + }, + fonts, + appearance, + }, }); element.replaceChildren(iframe); } -export interface TemplateEditorConfig { - features?: TemplateEditorFeatures | undefined; - fonts?: FontSource[] | undefined; - appearance?: AppearanceConfig | undefined; -} - -function validateFeatures(features: TemplateEditorFeatures): void { - if (features && Object.keys(features).length === 0) { - throw new Error("please specify at least one template editor feature."); - } -} - const REQUIRED_PERMISSIONS: SdkPermission[][] = [ ["workspace:read", "workspace:write"], ["user:read", "user:write"], ["template:write", "template:read"], ]; - -export interface TemplateEditorFeatures { - logo?: boolean; - icon?: boolean; - heading?: boolean; - colors?: boolean; - coverImage?: boolean; - headerField?: boolean; - secondaryFields?: boolean; - barcodeType?: boolean; - barcodeData?: boolean; - barcodeText?: boolean; - description?: boolean; - backFields?: boolean; - locationMessages?: boolean; - appLinks?: boolean; - expiry?: boolean; -} - -export const TEMPLATE_EDITOR_FEATURES_DEFAULT = { - logo: true, - icon: true, - heading: true, - colors: true, - coverImage: true, - headerField: true, - secondaryFields: true, - barcodeType: false, - barcodeData: false, - barcodeText: false, - description: false, - backFields: true, - locationMessages: true, - appLinks: true, - expiry: false, -} satisfies Record; - -export const TEMPLATE_EDITOR_FEATURES_ALL: Record< - keyof TemplateEditorFeatures, - true -> = { - logo: true, - icon: true, - heading: true, - colors: true, - coverImage: true, - headerField: true, - secondaryFields: true, - barcodeType: true, - barcodeData: true, - barcodeText: true, - description: true, - backFields: true, - locationMessages: true, - appLinks: true, - expiry: true, -}; - -export const TEMPLATE_EDITOR_FEATURES_NONE: Record< - keyof TemplateEditorFeatures, - false -> = { - logo: false, - icon: false, - heading: false, - colors: false, - coverImage: false, - headerField: false, - secondaryFields: false, - barcodeType: false, - barcodeData: false, - barcodeText: false, - description: false, - backFields: false, - locationMessages: false, - appLinks: false, - expiry: false, -}; - -export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< - Record -> = { - barcodeType: true, - barcodeData: true, - barcodeText: true, -}; diff --git a/src/calls/embedTemplatePage.ts b/src/calls/embedTemplatePage.ts index 28ada99..9d4b821 100644 --- a/src/calls/embedTemplatePage.ts +++ b/src/calls/embedTemplatePage.ts @@ -7,6 +7,7 @@ import { validatePermissions, type SdkPermission, } from "../helpers/permissions.ts"; +import type {TemplateEmbedFeatures} from "../helpers/embedFeatures.ts"; export interface EmbedTemplatePageOptions { /** @@ -41,19 +42,6 @@ export function embedTemplatePage( element.replaceChildren(iframe); } -export interface TemplateEmbedConfig { - features?: TemplateEmbedFeatures | undefined; - fonts?: FontSource[] | undefined; - appearance?: AppearanceConfig | undefined; -} - -export interface TemplateEmbedFeatures { - passList?: boolean; - templateEditor?: boolean; - campaigns?: boolean; - campaignEditor?: boolean; -} - function validatePermissionsByFeatures( permissions: SdkPermission[], features: TemplateEmbedFeatures, diff --git a/src/helpers/embedFeatures.ts b/src/helpers/embedFeatures.ts new file mode 100644 index 0000000..d904b8c --- /dev/null +++ b/src/helpers/embedFeatures.ts @@ -0,0 +1,92 @@ +export interface TemplateEmbedFeatures { + passList?: boolean; + templateEditor?: boolean | TemplateEditorFeatures; + campaigns?: boolean; + campaignEditor?: boolean; +} + +export interface TemplateEditorFeatures { + logo?: boolean; + icon?: boolean; + heading?: boolean; + colors?: boolean; + coverImage?: boolean; + headerField?: boolean; + secondaryFields?: boolean; + barcodeType?: boolean; + barcodeData?: boolean; + barcodeText?: boolean; + description?: boolean; + backFields?: boolean; + locationMessages?: boolean; + appLinks?: boolean; + expiry?: boolean; +} + +export const TEMPLATE_EDITOR_FEATURES_DEFAULT = { + logo: true, + icon: true, + heading: true, + colors: true, + coverImage: true, + headerField: true, + secondaryFields: true, + barcodeType: false, + barcodeData: false, + barcodeText: false, + description: false, + backFields: true, + locationMessages: true, + appLinks: true, + expiry: false, +} satisfies Record; + +export const TEMPLATE_EDITOR_FEATURES_ALL: Record< + keyof TemplateEditorFeatures, + true +> = { + logo: true, + icon: true, + heading: true, + colors: true, + coverImage: true, + headerField: true, + secondaryFields: true, + barcodeType: true, + barcodeData: true, + barcodeText: true, + description: true, + backFields: true, + locationMessages: true, + appLinks: true, + expiry: true, +}; + +export const TEMPLATE_EDITOR_FEATURES_NONE: Record< + keyof TemplateEditorFeatures, + false +> = { + logo: false, + icon: false, + heading: false, + colors: false, + coverImage: false, + headerField: false, + secondaryFields: false, + barcodeType: false, + barcodeData: false, + barcodeText: false, + description: false, + backFields: false, + locationMessages: false, + appLinks: false, + expiry: false, +}; + +export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< + Record +> = { + barcodeType: true, + barcodeData: true, + barcodeText: true, +}; diff --git a/src/index.ts b/src/index.ts index 742273e..ce74420 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,17 @@ export type {BadgeSdk} from "./sdk.ts"; export { embedTemplatePage, type EmbedTemplatePageOptions, - type TemplateEmbedFeatures, } from "./calls/embedTemplatePage.ts"; export { embedTemplateEditorPage, type EmbedTemplateEditorPageOptions, +} from "./calls/embedTemplateEditorPage.ts"; +export {makeSdk, type SdkOptions} from "./calls/makeSdk.ts"; +export { + type TemplateEmbedFeatures, type TemplateEditorFeatures, TEMPLATE_EDITOR_FEATURES_DEFAULT, TEMPLATE_EDITOR_FEATURES_ALL, TEMPLATE_EDITOR_FEATURES_NONE, TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE, -} from "./calls/embedTemplateEditorPage.ts"; -export {makeSdk, type SdkOptions} from "./calls/makeSdk.ts"; +} from "./helpers/embedFeatures.ts"; diff --git a/src/utils/iframe.ts b/src/utils/iframe.ts index 296f25c..9ef8378 100644 --- a/src/utils/iframe.ts +++ b/src/utils/iframe.ts @@ -1,11 +1,15 @@ -import type {TemplateEditorConfig} from "../calls/embedTemplateEditorPage"; -import type {TemplateEmbedConfig} from "../calls/embedTemplatePage"; +import type {TemplateEmbedFeatures} from "../helpers/embedFeatures.ts"; +import type {FontSource, AppearanceConfig} from "../helpers/appearance"; interface CreateIframeOptions { token: string; path: string; pathHash: string; - config: TemplateEmbedConfig | TemplateEditorConfig; + config: { + features?: TemplateEmbedFeatures | undefined; + fonts?: FontSource[] | undefined; + appearance?: AppearanceConfig | undefined; + }; } export function createEmbedIframe( From c8c3b53e041f1a03cd820a7941df802355f94b4f Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Fri, 23 Jan 2026 17:08:31 -0800 Subject: [PATCH 6/9] embedType QP and pr comments --- playground/Playground.tsx | 83 +++++++++++++--------------- src/calls/embedTemplateEditorPage.ts | 79 +++++++++++++++++++++++--- src/calls/embedTemplatePage.ts | 1 + src/helpers/embedFeatures.ts | 70 +---------------------- src/helpers/token.ts | 2 +- src/index.ts | 15 +++-- src/utils/iframe.ts | 11 +++- 7 files changed, 133 insertions(+), 128 deletions(-) diff --git a/playground/Playground.tsx b/playground/Playground.tsx index b736598..6b17e9f 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -116,33 +116,39 @@ export function Playground() { }, }; - if (settings.embedType === "templateEditor") { - const options: badge.EmbedTemplateEditorPageOptions = { - templateId, - features: - settings.editorFeatures ?? badge.TEMPLATE_EDITOR_FEATURES_DEFAULT, - fonts, - appearance, - }; - badge.embedTemplateEditorPage(sdk, element, options); - setSdkCall({ - sdkOptions, - options, - functionName: "embedTemplateEditorPage", - }); - } else { - const options: badge.EmbedTemplatePageOptions = { - templateId, - features: settings.features, - fonts, - appearance, - }; - badge.embedTemplatePage(sdk, element, options); - setSdkCall({ - sdkOptions, - options, - functionName: "embedTemplatePage", - }); + switch (settings.embedType) { + case "templateEditor": { + const options: badge.EmbedTemplateEditorPageOptions = { + templateId, + features: + settings.editorFeatures ?? + badge.TEMPLATE_EDITOR_FEATURES_DEFAULT, + fonts, + appearance, + }; + badge.embedTemplateEditorPage(sdk, element, options); + setSdkCall({ + sdkOptions, + options, + functionName: "embedTemplateEditorPage", + }); + return; + } + case "templateDetails": { + const options: badge.EmbedTemplatePageOptions = { + templateId, + features: settings.features, + fonts, + appearance, + }; + badge.embedTemplatePage(sdk, element, options); + setSdkCall({ + sdkOptions, + options, + functionName: "embedTemplatePage", + }); + return; + } } }) .catch(alert); @@ -231,18 +237,13 @@ export function Playground() { /> { settingChanged("permissions", value); }} /> - {settings.embedType === "templateDetails" ? ( + {settings.embedType === "templateDetails" && ( - ) : ( + )} + {settings.embedType === "templateEditor" && ( ; + +export const TEMPLATE_EDITOR_FEATURES_ALL: Record< + keyof TemplateEditorFeatures, + true +> = { + logo: true, + icon: true, + heading: true, + colors: true, + coverImage: true, + headerField: true, + secondaryFields: true, + barcodeType: true, + barcodeData: true, + barcodeText: true, + description: true, + backFields: true, + locationMessages: true, + appLinks: true, + expiry: true, +}; + +export const TEMPLATE_EDITOR_FEATURES_NONE: Record< + keyof TemplateEditorFeatures, + false +> = { + logo: false, + icon: false, + heading: false, + colors: false, + coverImage: false, + headerField: false, + secondaryFields: false, + barcodeType: false, + barcodeData: false, + barcodeText: false, + description: false, + backFields: false, + locationMessages: false, + appLinks: false, + expiry: false, +}; + +export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< + Record +> = { + barcodeType: true, + barcodeData: true, + barcodeText: true, +}; diff --git a/src/calls/embedTemplatePage.ts b/src/calls/embedTemplatePage.ts index 9d4b821..d74e0d2 100644 --- a/src/calls/embedTemplatePage.ts +++ b/src/calls/embedTemplatePage.ts @@ -36,6 +36,7 @@ export function embedTemplatePage( token: sdk.token, path: sdk.path, pathHash: `/${workspaceHandle}/templates/${templateId}/overview`, + embedType: "templateDetails", config: {features, fonts, appearance}, }); diff --git a/src/helpers/embedFeatures.ts b/src/helpers/embedFeatures.ts index d904b8c..3ffa65d 100644 --- a/src/helpers/embedFeatures.ts +++ b/src/helpers/embedFeatures.ts @@ -1,3 +1,5 @@ +export type EmbedType = "templateDetails" | "templateEditor"; + export interface TemplateEmbedFeatures { passList?: boolean; templateEditor?: boolean | TemplateEditorFeatures; @@ -22,71 +24,3 @@ export interface TemplateEditorFeatures { appLinks?: boolean; expiry?: boolean; } - -export const TEMPLATE_EDITOR_FEATURES_DEFAULT = { - logo: true, - icon: true, - heading: true, - colors: true, - coverImage: true, - headerField: true, - secondaryFields: true, - barcodeType: false, - barcodeData: false, - barcodeText: false, - description: false, - backFields: true, - locationMessages: true, - appLinks: true, - expiry: false, -} satisfies Record; - -export const TEMPLATE_EDITOR_FEATURES_ALL: Record< - keyof TemplateEditorFeatures, - true -> = { - logo: true, - icon: true, - heading: true, - colors: true, - coverImage: true, - headerField: true, - secondaryFields: true, - barcodeType: true, - barcodeData: true, - barcodeText: true, - description: true, - backFields: true, - locationMessages: true, - appLinks: true, - expiry: true, -}; - -export const TEMPLATE_EDITOR_FEATURES_NONE: Record< - keyof TemplateEditorFeatures, - false -> = { - logo: false, - icon: false, - heading: false, - colors: false, - coverImage: false, - headerField: false, - secondaryFields: false, - barcodeType: false, - barcodeData: false, - barcodeText: false, - description: false, - backFields: false, - locationMessages: false, - appLinks: false, - expiry: false, -}; - -export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< - Record -> = { - barcodeType: true, - barcodeData: true, - barcodeText: true, -}; diff --git a/src/helpers/token.ts b/src/helpers/token.ts index 10ce4e3..c0f0fee 100644 --- a/src/helpers/token.ts +++ b/src/helpers/token.ts @@ -14,7 +14,7 @@ export function parseTokenPayload(token: string): SdkTokenPayload { } export type SdkTokenPayload = z.infer; -const sdkTokenPayloadSchema = z.object({ +export const sdkTokenPayloadSchema = z.object({ workspaceHandle: z.string(), permissions: z.array(sdkPermissionSchema), }); diff --git a/src/index.ts b/src/index.ts index ce74420..df7bc7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,13 +7,20 @@ export { export { embedTemplateEditorPage, type EmbedTemplateEditorPageOptions, + TEMPLATE_EDITOR_FEATURES_DEFAULT, + TEMPLATE_EDITOR_FEATURES_ALL, + TEMPLATE_EDITOR_FEATURES_NONE, + TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE, } from "./calls/embedTemplateEditorPage.ts"; export {makeSdk, type SdkOptions} from "./calls/makeSdk.ts"; export { type TemplateEmbedFeatures, type TemplateEditorFeatures, - TEMPLATE_EDITOR_FEATURES_DEFAULT, - TEMPLATE_EDITOR_FEATURES_ALL, - TEMPLATE_EDITOR_FEATURES_NONE, - TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE, } from "./helpers/embedFeatures.ts"; +export { + type AppearanceConfig, + type FontSource, + type CssFontSource, + type CustomFontSource, +} from "./helpers/appearance.ts"; +export {type SdkTokenPayload} from "./helpers/token.ts"; diff --git a/src/utils/iframe.ts b/src/utils/iframe.ts index 9ef8378..1c60add 100644 --- a/src/utils/iframe.ts +++ b/src/utils/iframe.ts @@ -1,10 +1,14 @@ -import type {TemplateEmbedFeatures} from "../helpers/embedFeatures.ts"; +import type { + EmbedType, + TemplateEmbedFeatures, +} from "../helpers/embedFeatures.ts"; import type {FontSource, AppearanceConfig} from "../helpers/appearance"; -interface CreateIframeOptions { +export interface CreateIframeOptions { token: string; path: string; pathHash: string; + embedType: EmbedType; config: { features?: TemplateEmbedFeatures | undefined; fonts?: FontSource[] | undefined; @@ -15,7 +19,7 @@ interface CreateIframeOptions { export function createEmbedIframe( options: CreateIframeOptions, ): HTMLIFrameElement { - const {path, token, pathHash, config} = options; + const {path, token, pathHash, embedType, config} = options; const iframe = document.createElement("iframe"); @@ -26,6 +30,7 @@ export function createEmbedIframe( const iframeUrl = new URL(path); iframeUrl.searchParams.set("token", token); + iframeUrl.searchParams.set("embedType", embedType); iframeUrl.searchParams.set("config", JSON.stringify(config)); iframeUrl.hash = pathHash; From b16ec2f750d9a2a38d4a002d1e7f6bc2ec957032 Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Mon, 26 Jan 2026 17:57:06 -0800 Subject: [PATCH 7/9] changed embedType to navigation query param --- src/calls/embedTemplateEditorPage.ts | 2 +- src/calls/embedTemplatePage.ts | 2 +- src/helpers/embedFeatures.ts | 2 +- src/utils/iframe.ts | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/calls/embedTemplateEditorPage.ts b/src/calls/embedTemplateEditorPage.ts index f2e9ed2..8fc6c7c 100644 --- a/src/calls/embedTemplateEditorPage.ts +++ b/src/calls/embedTemplateEditorPage.ts @@ -32,7 +32,7 @@ export function embedTemplateEditorPage( token: sdk.token, path: sdk.path, pathHash: `/${workspaceHandle}/templates/${templateId}/edit`, - embedType: "templateEditor", + navigation: "bottom", config: { features: { templateEditor: diff --git a/src/calls/embedTemplatePage.ts b/src/calls/embedTemplatePage.ts index d74e0d2..f49a23f 100644 --- a/src/calls/embedTemplatePage.ts +++ b/src/calls/embedTemplatePage.ts @@ -36,7 +36,7 @@ export function embedTemplatePage( token: sdk.token, path: sdk.path, pathHash: `/${workspaceHandle}/templates/${templateId}/overview`, - embedType: "templateDetails", + navigation: "top", config: {features, fonts, appearance}, }); diff --git a/src/helpers/embedFeatures.ts b/src/helpers/embedFeatures.ts index 3ffa65d..615628d 100644 --- a/src/helpers/embedFeatures.ts +++ b/src/helpers/embedFeatures.ts @@ -1,4 +1,4 @@ -export type EmbedType = "templateDetails" | "templateEditor"; +export type Navigation = "top" | "bottom"; export interface TemplateEmbedFeatures { passList?: boolean; diff --git a/src/utils/iframe.ts b/src/utils/iframe.ts index 1c60add..ed997b1 100644 --- a/src/utils/iframe.ts +++ b/src/utils/iframe.ts @@ -1,5 +1,5 @@ import type { - EmbedType, + Navigation, TemplateEmbedFeatures, } from "../helpers/embedFeatures.ts"; import type {FontSource, AppearanceConfig} from "../helpers/appearance"; @@ -8,7 +8,7 @@ export interface CreateIframeOptions { token: string; path: string; pathHash: string; - embedType: EmbedType; + navigation: Navigation; config: { features?: TemplateEmbedFeatures | undefined; fonts?: FontSource[] | undefined; @@ -19,7 +19,7 @@ export interface CreateIframeOptions { export function createEmbedIframe( options: CreateIframeOptions, ): HTMLIFrameElement { - const {path, token, pathHash, embedType, config} = options; + const {path, token, pathHash, navigation, config} = options; const iframe = document.createElement("iframe"); @@ -30,7 +30,7 @@ export function createEmbedIframe( const iframeUrl = new URL(path); iframeUrl.searchParams.set("token", token); - iframeUrl.searchParams.set("embedType", embedType); + iframeUrl.searchParams.set("navigation", navigation); iframeUrl.searchParams.set("config", JSON.stringify(config)); iframeUrl.hash = pathHash; From 0aba2a0ca33f5af3d83a724612590c1aa987f86a Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Mon, 26 Jan 2026 18:33:28 -0800 Subject: [PATCH 8/9] type cleanup --- src/calls/embedTemplateEditorPage.ts | 62 +++++++--------------------- src/helpers/embedFeatures.ts | 2 + 2 files changed, 18 insertions(+), 46 deletions(-) diff --git a/src/calls/embedTemplateEditorPage.ts b/src/calls/embedTemplateEditorPage.ts index 8fc6c7c..2522ad1 100644 --- a/src/calls/embedTemplateEditorPage.ts +++ b/src/calls/embedTemplateEditorPage.ts @@ -4,7 +4,10 @@ import {createEmbedIframe} from "../utils/iframe.ts"; import {validatePermissions} from "../helpers/permissions.ts"; import {parseTokenPayload} from "../helpers/token.ts"; import {type SdkPermission} from "../helpers/permissions.ts"; -import {type TemplateEditorFeatures} from "../helpers/embedFeatures.ts"; +import { + type TemplateEditorFeature, + type TemplateEditorFeatures, +} from "../helpers/embedFeatures.ts"; export interface EmbedTemplateEditorPageOptions { /** @@ -70,54 +73,21 @@ export const TEMPLATE_EDITOR_FEATURES_DEFAULT = { locationMessages: true, appLinks: true, expiry: false, -} satisfies Record; +} satisfies Required; -export const TEMPLATE_EDITOR_FEATURES_ALL: Record< - keyof TemplateEditorFeatures, - true -> = { - logo: true, - icon: true, - heading: true, - colors: true, - coverImage: true, - headerField: true, - secondaryFields: true, - barcodeType: true, - barcodeData: true, - barcodeText: true, - description: true, - backFields: true, - locationMessages: true, - appLinks: true, - expiry: true, -}; +export const TEMPLATE_EDITOR_FEATURES_NONE = Object.fromEntries( + Object.keys(TEMPLATE_EDITOR_FEATURES_DEFAULT).map((key) => [key, false]), +) as Record; -export const TEMPLATE_EDITOR_FEATURES_NONE: Record< - keyof TemplateEditorFeatures, - false -> = { - logo: false, - icon: false, - heading: false, - colors: false, - coverImage: false, - headerField: false, - secondaryFields: false, - barcodeType: false, - barcodeData: false, - barcodeText: false, - description: false, - backFields: false, - locationMessages: false, - appLinks: false, - expiry: false, -}; +export const TEMPLATE_EDITOR_FEATURES_ALL = Object.fromEntries( + Object.keys(TEMPLATE_EDITOR_FEATURES_DEFAULT).map((key) => [key, true]), +) as Record; -export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE: Partial< - Record -> = { +export const TEMPLATE_EDITOR_FEATURES_ENABLE_BARCODE = { barcodeType: true, barcodeData: true, barcodeText: true, -}; +} satisfies Pick< + Record, + "barcodeType" | "barcodeData" | "barcodeText" +>; diff --git a/src/helpers/embedFeatures.ts b/src/helpers/embedFeatures.ts index 615628d..7d9f618 100644 --- a/src/helpers/embedFeatures.ts +++ b/src/helpers/embedFeatures.ts @@ -7,6 +7,8 @@ export interface TemplateEmbedFeatures { campaignEditor?: boolean; } +export type TemplateEditorFeature = keyof TemplateEditorFeatures; + export interface TemplateEditorFeatures { logo?: boolean; icon?: boolean; From ce7b53c6169b944bb7d66ce9885394b502d311b4 Mon Sep 17 00:00:00 2001 From: Illia Khomenko Date: Tue, 27 Jan 2026 14:03:58 -0800 Subject: [PATCH 9/9] addressed pr comments --- playground/Playground.tsx | 34 ++++++++++++++++++++-------- src/calls/embedTemplateEditorPage.ts | 4 ++-- src/calls/embedTemplatePage.ts | 2 +- src/{utils => helpers}/iframe.ts | 7 ++---- 4 files changed, 29 insertions(+), 18 deletions(-) rename src/{utils => helpers}/iframe.ts (84%) diff --git a/playground/Playground.tsx b/playground/Playground.tsx index 6b17e9f..df9fa90 100644 --- a/playground/Playground.tsx +++ b/playground/Playground.tsx @@ -24,10 +24,10 @@ import {useEffect, useRef, useState} from "react"; interface Settings { apiKey: string; templateId: string | null; - embedType: "templateDetails" | "templateEditor"; + embedType?: "templateDetails" | "templateEditor"; permissions: string[]; features: Required; - editorFeatures: badge.TemplateEditorFeatures; + editorFeatures?: badge.TemplateEditorFeatures; sdkPath: string; googleFont: string; primaryColor: string; @@ -43,9 +43,21 @@ export function Playground() { key: "state", defaultValue: DEFAULT_SETTINGS, serialize: JSON.stringify, - deserialize: JSON.parse as ( - value: string | undefined, - ) => typeof DEFAULT_SETTINGS, + deserialize: (value: string | undefined): Settings => { + if (!value) { + return DEFAULT_SETTINGS; + } + + const parsed = JSON.parse(value) as Partial; + + return { + ...DEFAULT_SETTINGS, + ...parsed, + embedType: parsed.embedType ?? "templateDetails", + editorFeatures: + parsed.editorFeatures ?? badge.TEMPLATE_EDITOR_FEATURES_DEFAULT, + }; + }, getInitialValueInEffect: false, }); @@ -116,7 +128,8 @@ export function Playground() { }, }; - switch (settings.embedType) { + const embedType = settings.embedType ?? "templateDetails"; + switch (embedType) { case "templateEditor": { const options: badge.EmbedTemplateEditorPageOptions = { templateId, @@ -190,6 +203,7 @@ export function Playground() { handleApiKeyChange(settings.apiKey); }, [handleApiKeyChange, settings.apiKey]); + const embedType = settings.embedType ?? "templateDetails"; return (