Skip to content

Connector Widget

The connector widget is a self-contained Vue 3 custom element (<conflux-connector>) that an embedding platform (e.g. Rapidmail) drops onto its own pages to let users connect their e-commerce store to Conflux — without ever leaving the embedding platform's UI.

It is distinct from the configuration widget (<conflux-config>): the configuration widget manages sync settings for an already-established connection; the connector widget handles the connection establishment itself.

Why a custom element

The widget is built with Vue 3's defineCustomElement and compiled to a single IIFE bundle (connector-widget.js). Embedding it requires one <script> tag and one HTML element — no npm install, no framework requirement, no style leakage in either direction thanks to the shadow DOM boundary.

Two-phase flow

A typical embed goes through two phases in sequence.

Phase 1 — Initialisation

On mount, if no tenant-id prop is present, the widget immediately calls POST /api/system-connections/connect with system_slug and external_user_id in the request body.

When the system_slug matches a known System record in Conflux (e.g. rapidmail), the endpoint:

  1. Auto-creates a new Tenant with a generated name.
  2. Creates a SystemConnection linked to that tenant with status = active and last_connected_at stamped immediately — no plugin handshake is needed because the embedding system is Conflux itself.
  3. Issues a never-expiring SystemConnectionAccessToken and returns it as token in the response alongside tenant_id.

The widget stores both values internally and fires the conflux-initialized event so the host page can persist them. Future page loads should pass tenant-id and token back as props, which skips phase 1 entirely.

sequenceDiagram
    participant Widget as <conflux-connector>
    participant Conflux
    participant HostPage as Host Page (JS)

    Widget->>Conflux: POST /api/system-connections/connect<br/>{ system_slug: "rapidmail", external_user_id: "..." }
    Conflux->>Conflux: create Tenant + active SystemConnection<br/>issue SystemConnectionAccessToken
    Conflux-->>Widget: 201 { tenant_id, token, connection }
    Widget->>HostPage: dispatchEvent("conflux-initialized")<br/>{ tenant_id, token }
    Note over HostPage: persist tenant_id + token<br/>for future page loads

Phase 2 — Plugin pairing

Once initialised, the widget shows the user platform-specific instructions and a "Generate connection token" button. When clicked, the widget calls POST /api/system-connections/connect again, this time:

  • With Authorization: Bearer <access-token> so the server resolves the tenant from the token rather than from a body param.
  • With an empty body (or { tenant_id } as fallback if no access token is available).

The endpoint creates a new SystemConnection with status = pending and returns a short-lived connection_token for the user to paste into their e-commerce plugin.

The widget then begins polling GET /api/widget/connections/{connection_id}/status every 3 seconds. When the plugin completes its side of the handshake and Conflux marks the connection active, the widget stops polling, shows a success banner, and fires the conflux-registered event.

sequenceDiagram
    participant Widget as <conflux-connector>
    participant Conflux
    participant Plugin as E-commerce Plugin
    participant HostPage as Host Page (JS)

    Widget->>Conflux: POST /api/system-connections/connect<br/>Authorization: Bearer <access-token>
    Conflux->>Conflux: resolve tenant from token<br/>create pending SystemConnection<br/>issue short-lived connection_token
    Conflux-->>Widget: 201 { connection_id, connection_token, expires_at }
    Widget->>Widget: show token + start polling

    loop every 3 s
        Widget->>Conflux: GET /api/widget/connections/{id}/status<br/>Authorization: Bearer <access-token>
        Conflux-->>Widget: { id, status: "pending" }
    end

    Note over Plugin: merchant pastes token into plugin
    Plugin->>Conflux: POST /api/system-connections/register<br/>connection_token + credentials
    Conflux->>Conflux: verify + activate connection

    Widget->>Conflux: GET /api/widget/connections/{id}/status
    Conflux-->>Widget: { id, status: "active" }
    Widget->>Widget: stop polling, show success
    Widget->>HostPage: dispatchEvent("conflux-registered")<br/>{ connection_id }

Access tokens vs connection tokens

Two different token types are involved.

SystemConnectionAccessToken SystemConnectionToken
Table system_connection_access_tokens system_connection_tokens
Issued when Phase 1 init (Active direct-system connection) Phase 2 "Generate token" button
Lifetime Never expires 1 hour (configurable)
Purpose Authenticate subsequent widget API calls Plugin-pairing handshake (one-time use)
Returned to Host page via conflux-initialized User (shown in widget UI for copy-paste)

The access token is returned in the conflux-initialized event detail only once. The host page must persist it (e.g. in its own database keyed to the user). If lost, the user must re-initialise the widget to obtain a new one.

Events

Both events use bubbles: true, composed: true so they pierce the shadow DOM boundary and can be caught on window or any ancestor element.

Events are dispatched from the host element itself (this.$el.getRootNode().host), falling back to this.$el when no shadow root host is present. This matters for Shadow DOM consumers that attach listeners directly to the <conflux-connector> element rather than to window.

conflux-initialized

window.addEventListener('conflux-initialized', (event) => {
    const { tenant_id, token } = event.detail;
    // Persist these; pass them as props on subsequent loads.
});

Fires once at the end of phase 1, when the widget has created (or confirmed) a tenant and an active connection to the embedding system. The token value is the never-expiring access token and is returned only this once.

conflux-registered

window.addEventListener('conflux-registered', (event) => {
    const { connection_id } = event.detail;
    // The e-commerce plugin is now connected.
});

Fires when polling detects status = active on the pending connection — i.e. when the e-commerce plugin has completed its side of the pairing handshake.

Widget phases

Internally the widget moves through three display phases:

Phase Trigger UI
initializing On mount, no tenant-id prop present Spinner; error state with retry if the init call fails
instructing Init completed (or tenant-id prop was already set) Plugin CTA, generate-token button, token display, paste instructions, "What's next" panel, polling status
connected Only for the direct-system init (phase 1 created an Active connection with no plugin) Success banner immediately — no phase 2 needed

The connected phase is only reached via phase 1 when the system_slug maps to a direct system (like rapidmail). For e-commerce plugin connections, the instructing phase persists while polling waits for the plugin.

UX sections in the instructing phase

The instructing phase renders several distinct sections.

Intro and plugin CTA. A short description ("You'll need the Positive Connector plugin…") appears above the token area. A "Get the Positive Connector plugin" external link is always rendered below it. When PLUGIN_URLS[connection][system] resolves to a non-null URL the link goes to that URL; otherwise it falls back to href="../" (effectively a no-op). Set a real URL in plugin-urls.js to activate the link for a given combination.

Token area. The "Generate connection token" button triggers phase 2. Once a token is generated, it is displayed in a read-only copy-paste input alongside its expiry time. A "Generate a new token" link allows re-generation.

"Where do I paste this?" disclosure. A labelled section below the token area lists the paste steps for the active connection type. Steps are driven by the PASTE_STEPS configuration object in the source file and rendered as a numbered list with bold navigation labels. A generic fallback is shown when connection does not match a known key.

Success banner. When connected becomes true (polling detects status: active), a green "Plugin connected successfully!" banner is shown within the instructing phase.

"What's next" disclosure. A two-state info block:

Widget state Text shown
Not yet connected (connected === false) Muted: "Once connected, we'll sync your data based on the default settings…"
Connected (connected === true) Active: "We're syncing your data based on the default settings…" + settings breadcrumb from SETTINGS_PATH[connection]

The settings breadcrumb (e.g. WooCommerce → Positive Connector → Sync settings) is driven by the SETTINGS_PATH configuration object and is hidden when the key is absent.

Configuration constants

Two constants at the top of ConfluxConnector.vue and one in a companion file are the intended customisation points for new embedding systems or shop platforms:

Constant Location Keyed by Purpose
PLUGIN_URLS resources/widget-connector/plugin-urls.js connectionsystem Plugin download URL per shop platform and embedding system. Use default as the fallback key. null hides the CTA. Baked in at build time by Vite.
PASTE_STEPS ConfluxConnector.vue connection Numbered HTML steps for the "Where do I paste this?" section. Falls back to GENERIC_PASTE_STEPS when the key is absent.
SETTINGS_PATH ConfluxConnector.vue connection Admin breadcrumb string shown in the connected "What's next" state. Hidden when the key is absent.

PLUGIN_URLS was deliberately separated into its own file so it can be updated without touching the Vue component. The structure inside plugin-urls.js is:

{
    woocommerce: {
        default:   null,   // fallback when system prop does not match any key
        rapidmail: null,   // embedding-system-specific URL
    },
    prestashop: {
        default:   null,
        rapidmail: null,
    },
}

Set any value to a non-null string to enable the "Get the Positive Connector plugin" CTA link for that combination. Rebuild the bundle after editing this file.

Widget polling

Polling uses GET /api/widget/connections/{connection}/status inside the widget-auth middleware group. The route is protected by AuthenticateWidgetToken, which resolves the SystemConnection from the Bearer access token and then checks that the requested connection belongs to the same tenant before returning its status. The widget stops polling as soon as it receives status: active or when the component is unmounted.

Build

docker compose run --rm -T node npm run build:connector-widget

This runs vite build --config vite.connector-widget.config.js and writes the IIFE bundle to public/connector-widget.js. Run this after editing plugin-urls.js or any other file under resources/widget-connector/ — Vite bakes those values in at build time.

Source files

File Purpose
resources/widget-connector/ConfluxConnector.vue Vue 3 custom element — template, phase logic, polling, event dispatch, scoped styles
resources/widget-connector/plugin-urls.js Plugin download URL map (connection → system → URL); edit this to activate the CTA link
resources/widget-connector/main.js Entry point — calls defineCustomElement and registers <conflux-connector>
vite.connector-widget.config.js Vite IIFE build config; outputs public/connector-widget.js
app/Http/Controllers/Api/ConnectSystemConnectionController.php POST /api/system-connections/connect — both init and generate-token paths
app/Http/Controllers/Api/Widget/ConnectionStatusController.php GET /api/widget/connections/{connection}/status
app/Http/Middleware/AuthenticateWidgetToken.php Bearer token resolution for widget routes
resources/views/configuration-widget.blade.php Interactive dev playground (two-step setup form + event log)
resources/views/widget-connector-preview.blade.php Minimal standalone preview page

See also