Skip to content

Dealer experience plugin

This example builds a standalone dealer experience that highlights a preferred brand, adds a help banner, opens a shopping modal, forwards analytics data, and presents curated vehicle builds.

It uses native browser APIs, so its only required browser dependency is the public plugin SDK.

Project structure

text
dealer-plugin/
├── package.json
├── tsconfig.json
├── vite.config.ts
└── src/
    ├── plugin.ts
    └── preboot.ts

Install the versioned SDK package supplied for plugin development:

bash
npm install ./iconfigurators-plugin-sdk-VERSION.tgz
npm install --save-dev typescript vite

Define deployment options

Options keep customer-specific labels and identifiers outside the reusable plugin definition.

ts
import {
  PLUGIN_API_VERSION,
  definePlugin,
  type PluginBrandRecord,
} from '@iconfigurators/plugin-sdk'

export type DealerExperienceOptions = {
  dealerName?: string
  featuredBrandSlug?: string
  badgeLabel?: string
  bannerTitle?: string
  bannerBody?: string
}

type DealerBrand = PluginBrandRecord & {
  dealerFeatured?: boolean
}

Use brands:filter to return decorated copies of the existing records. The extra field is then available to the typed brand-card outlet.

ts
api.hooks.filter('brands:filter', (brands, context) => {
  if (context.source !== 'brands') return brands

  return brands.map<DealerBrand>((brand) => ({
    ...brand,
    dealerFeatured: brand.slug?.toLowerCase() === options.featuredBrandSlug.toLowerCase(),
  }))
})

api.ui.mount('brands:card:badges', (container, context) => {
  const brand = context.brand as DealerBrand
  if (!brand.dealerFeatured) return

  const badge = document.createElement('span')
  badge.className = 'dealer-experience-badge'
  badge.textContent = options.badgeLabel
  container.appendChild(badge)

  return () => badge.remove()
})

Return a new array and record rather than changing the received brand objects.

Forward an event to the host page

The plugin can translate a configurator event into a customer-owned browser event. Send only the fields the host integration needs.

ts
api.events.on('brands:loaded', (event) => {
  const featuredBrand = event.data.brands.find(
    (brand) => brand.slug?.toLowerCase() === options.featuredBrandSlug.toLowerCase(),
  )

  window.dispatchEvent(
    new CustomEvent('dealer-experience:brands-loaded', {
      detail: {
        featuredBrandId: featuredBrand?.id ?? null,
        resultCount: event.data.resultCount,
      },
    }),
  )
})

The embedding website can listen without depending on the plugin SDK:

js
window.addEventListener('dealer-experience:brands-loaded', (event) => {
  console.log(event.detail.featuredBrandId, event.detail.resultCount)
})

Add a dealer-help modal

Mount a banner into brands:top and use api.ui.openModal() for the workflow. Icon Visualizer owns the dialog shell while the plugin owns the modal body.

ts
api.ui.mount('brands:top', (container) => {
  const banner = document.createElement('aside')
  const title = document.createElement('strong')
  const button = document.createElement('button')

  banner.className = 'dealer-experience-banner'
  title.textContent = options.bannerTitle
  button.type = 'button'
  button.textContent = 'Get shopping help'
  banner.append(title, button)

  let closeModal: (() => void) | undefined

  const showHelp = () => {
    closeModal?.()
    closeModal = api.ui.openModal({
      id: 'dealer-experience-help',
      title: `Shop with ${options.dealerName}`,
      description: 'Choose how you want to begin.',
      size: 'medium',
      render(modalContainer, context) {
        const chooseVehicle = document.createElement('button')
        chooseVehicle.type = 'button'
        chooseVehicle.textContent = 'Shop by vehicle'

        const openSearch = () => {
          api.commands.openSearchByVehicle()
          context.close('api')
        }

        chooseVehicle.addEventListener('click', openSearch)
        modalContainer.appendChild(chooseVehicle)

        return () => {
          chooseVehicle.removeEventListener('click', openSearch)
          chooseVehicle.remove()
        }
      },
    })
  }

  button.addEventListener('click', showHelp)
  container.appendChild(banner)

  return () => {
    closeModal?.()
    button.removeEventListener('click', showHelp)
    banner.remove()
  }
})

Add curated vehicle builds

Register a complete landing section when the plugin owns content larger than a small outlet contribution. runAction() executes a documented action and records the item selection.

ts
api.ui.registerLandingSection({
  id: 'customer-builds',
  placement: 'between-featured-brands-and-new-arrivals',
  order: 10,
  title: 'Customer builds',
  description: 'Explore vehicle and wheel combinations selected by the dealer team.',
  render(container, context) {
    const list = document.createElement('ul')
    const builds = [
      {
        id: 'FEATURED_BUILD_ID',
        title: 'Featured vehicle build',
        query: {
          fmk: 'VEHICLE_FMK',
          bodyType: 'BODY_TYPE_ID',
          sizeCategory: 'OE',
          wheel: 'WHEEL_IMAGE_ID',
          wheelDiameter: 'WHEEL_DIAMETER',
        },
      },
    ]

    for (const build of builds) {
      const item = document.createElement('li')
      const button = document.createElement('button')
      button.type = 'button'
      button.textContent = build.title
      button.addEventListener('click', () => {
        void context.runAction(build.id, {
          command: 'openVisualizer',
          query: build.query,
        })
      })
      item.appendChild(button)
      list.appendChild(item)
    }

    container.appendChild(list)
    return () => list.remove()
  },
})

For a production carousel, retain each listener cleanup and remove them before removing the section content.

Register styles

Register styles with the plugin API so they apply to plugin UI inside the configurator. Prefix classes with the plugin name to prevent collisions.

ts
api.ui.addStyle(`
  .dealer-experience-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid #cbd5e1;
    border-radius: 0.5rem;
  }

  .dealer-experience-badge {
    padding: 0.25rem 0.5rem;
    border-radius: 999px;
    background: #2563eb;
    color: white;
    font-weight: 700;
  }
`)

Assemble the plugin

Place the registrations inside one plugin definition. Default options make missing optional values predictable.

ts
export default definePlugin<DealerExperienceOptions>({
  id: 'customer.dealer-experience',
  version: '1.0.0',
  apiVersion: PLUGIN_API_VERSION,

  setup(api, configuredOptions) {
    const options = {
      dealerName: 'Dealer',
      featuredBrandSlug: 'FEATURED_BRAND_SLUG',
      badgeLabel: 'Dealer pick',
      bannerTitle: 'Need help choosing a wheel?',
      bannerBody: 'A specialist can help you compare compatible options.',
      ...configuredOptions,
    }

    // Register the hook, events, UI, and styles shown above here.
  },
})

Queue customer-specific options

The preboot entry supplies deployment-specific values without changing the reusable plugin.

ts
// src/preboot.ts
import { queuePlugin } from '@iconfigurators/plugin-sdk'
import dealerExperiencePlugin from './plugin'

queuePlugin(dealerExperiencePlugin, {
  order: 20,
  required: false,
  options: {
    dealerName: 'YOUR_DEALER_NAME',
    featuredBrandSlug: 'FEATURED_BRAND_SLUG',
    badgeLabel: 'Recommended',
    bannerTitle: 'Need help choosing the right wheel?',
    bannerBody: 'A specialist can help you compare compatible options.',
  },
})

Build the browser artifact

ts
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/preboot.ts',
      name: 'DealerExperiencePlugin',
      formats: ['iife'],
      fileName: () => 'dealer-experience.plugin.js',
    },
  },
})

The output must include the SDK helpers and all plugin-owned dependencies.

Load and verify

html
<script src="https://YOUR_DOMAIN/plugins/dealer-experience.plugin.js"></script>
<div id="icf_page"></div>
<script src="https://iconfigurators.app/src/embed.cfm?ky=CONFIGURATOR_KEY"></script>
js
document.addEventListener('iconfigurator.ready', () => {
  const registration = window.iConfigurator.getPluginStatus('customer.dealer-experience')
  console.assert(registration?.status === 'ready')
})

Continue with Test a plugin and Release and rollback before enabling the plugin on a production page.

Icon Visualizer developer documentation