@clickterm/widget
v2.7.1
Published
Browser SDK (TypeScript) that embeds Clickterm clickwrap agreements into host pages as a modal dialog or inline checkbox.
Maintainers
Readme
@clickterm/widget
Browser SDK (TypeScript) that embeds Clickterm clickwrap agreements into host pages, either as a modal dialog or an inline checkbox.
Install
npm install @clickterm/widgetOr load it directly from a CDN (UMD build, exposes window.Clickterm):
<script src="https://unpkg.com/@clickterm/widget"></script>
<!-- or -->
<script src="https://cdn.jsdelivr.net/npm/@clickterm/widget"></script>Quick start
Dialog (modal) — with a bundler
import { ClicktermClient, ClicktermDialog } from '@clickterm/widget';
ClicktermClient.initialize('YOUR_APP_ID');
const result = await ClicktermDialog.show(
{
endUserId: 'user_12345',
clickwrapTemplateId: 'YOUR_TEMPLATE_ID',
templatePlaceholders: {
fullName: "Ada",
customPlaceholders: {
"custom1": "Custom 1"
}
}
},
{},
{
onSuccess: (signature) => console.log('signed:', signature),
onAlreadyAccepted: () => console.log('already accepted'),
onCancel: () => console.log('dismissed'),
onError: (err) => console.error(err),
onComplete: (result) => console.log('dialog complete:', result),
},
);Bundle dialog
Initialize the client once, then pass the bundle ID, your stable end-user ID, the requested language, and any placeholders grouped by template ID:
import {
ClicktermClient,
ClicktermDialog,
type ClicktermDialogError,
} from '@clickterm/widget';
ClicktermClient.initialize('YOUR_APP_ID');
// Development: ClicktermClient.initialize('YOUR_APP_ID', 'https://api.dev.clickterm.com');
const result = await ClicktermDialog.showBundle(
{
clickwrapBundleId: 'YOUR_BUNDLE_ID',
endUserId: 'user_12345',
language: 'en',
templatePlaceholdersByTemplateId: {
'TEMPLATE_ID': {
fullName: 'Ada Lovelace',
customPlaceholders: { plan: 'Pro' },
},
},
},
{},
{
onSuccess: (signature) => {
// Forward the signature to your backend for verification.
console.log('bundle signature:', signature);
},
onAlreadyAccepted: () => console.log('bundle already accepted'),
onCancel: () => console.log('bundle canceled'),
onError: (error: ClicktermDialogError) => {
console.error(error.code, error.status, error.message);
if (error.status === 410) {
// The rendered bundle snapshot is stale. Start a new bundle Request.
}
},
onComplete: (terminalResult) => {
// Runs once after success, already accepted, cancellation, or terminal error.
console.log('bundle complete:', terminalResult);
},
},
);
console.log(result.clicktermSignature);All listeners are optional; the returned promise remains authoritative. Listener exceptions are isolated from the SDK result. The lifecycle ordering is:
| Outcome | Callbacks | Promise |
|---|---|---|
| Accepted or declined | onSuccess(signature), then onComplete(result) | Resolves with the signature result |
| Already accepted | onAlreadyAccepted(), then onComplete(result) | Resolves with isAlreadyAccepted: true |
| User canceled | onCancel(), then onComplete(result) | Resolves with isCanceled: true |
| Retryable load or Apply error | onError(error) | Dialog remains active; no completion yet |
| Terminal error | onError(error), then onComplete(result) | Rejects with the same structured error |
onError receives an Error with optional code and HTTP status. Use
onCancel, rather than inferring cancellation from an error fallback result, to
classify an explicit user cancellation.
The bundle UI displays reviewable documents in order and unlocks later documents
as the user proceeds. Accept requires every pending required document to be
selected; unchecked optional documents are submitted as declined. Decline
All is a separate action and declines every pending required and optional
document. Only PENDING items are submitted—existing ACCEPTED and UNVERIFIED
evidence is represented by the signed bundle request token. Previously accepted
content is displayed checked and locked.
Bundle SCROLL/CHECKBOX agreement mode comes from the bundle customization.
Visual theme and logo settings come from the bundle's first template. Bundle UI
copy uses the first resolved template language and falls back to the requested
language; translations are loaded from the environment-specific Clickterm CDN.
The dialog exposes accessible headings, document names, requirement context, and
labeled checkbox controls to screen readers.
Inline (checkbox in your own form) — via script tag
<div id="my-consent"></div>
<script src="https://unpkg.com/@clickterm/widget"></script>
<script>
const { ClicktermClient, ClicktermDom } = window.Clickterm;
ClicktermClient.initialize('YOUR_APP_ID');
ClicktermDom.renderInline('my-consent', {
endUserId: 'user_12345',
clickwrapTemplateId: 'YOUR_TEMPLATE_ID',
}, {
// Optional lifecycle signals — the SDK renders no loading/error UI of its own.
onLoading: () => showConsentLoader(), // request in flight
onReady: ({ outcome }) => hideConsentLoader(), // 'RENDERED' | 'ALREADY_ACCEPTED' | 'EXISTING_SIGNATURE'
onError: (err) => showConsentError(err), // same error the promise rejects with
}).then((handle) => {
document.getElementById('my-form').addEventListener('submit', async (e) => {
e.preventDefault();
const result = await handle.finalize();
console.log(result.status, result.clicktermSignature);
});
}).catch((err) => {
// onError updates the host UI; the rejection still needs handling.
console.error('Failed to render inline clickwrap:', err);
});
</script>See docs/inline-clickwrap.md for the full inline integration guide, including the comprehensive semantic theme reference, validation rules, multiple clickwraps, placeholders, and edge cases.
Public API
All exports live on src/index.ts. Three static classes:
ClicktermClient.initialize(appId, baseUrl?)— configures the SDK. Must be called first.ClicktermDialog.show(request, config?, listeners?)/showBundle(request, config?, listeners?)/showAcceptedContent(request, config?)— modal flows.ClicktermDom.renderInline(containerId, request, options?)/finalizeAll(containerIds?)— inline checkbox flow. InlineoptionsacceptonChange,style, and the lifecycle callbacksonLoading/onReady/onError(onReadyreports the render outcome:RENDERED,ALREADY_ACCEPTED, orEXISTING_SIGNATURE).formatTimestamp(timestamp, settings?, includeTime?)— formats content timestamps using the response timezone, date, and clock settings.formatNumber(value, settings?, decimalPlaces?)— formats a number usingnumberFormat; precision defaults to two decimal places.formatCurrency(value, currencyCode, settings?, decimalPlaces?)— formats the supplied ISO 4217 currency using the independentcurrencyNumberFormat,currencyDisplay, andcurrencyPositionsettings; precision defaults to two decimal places.
Clickwrap and bundle responses expose optional formattingSettings only at the response root. Content objects do not contain a separate formatting snapshot. Older backend versions may omit this metadata; the SDK preserves its legacy date formatting in that case.
The numeric formatters accept an explicit precision at each call site because the required precision depends on the displayed value rather than organization settings. Missing formatting metadata falls back to 1,234.50 and the default separators, symbol display, and before-amount position for the supplied currency.
TypeScript types for every request/response/option shape ship with the package.
Built-in HTTP retry policy
The SDK applies a fixed, internal retry policy to read/setup traffic only. It is not publicly configurable.
Eligible calls are:
POST /public-client/v1/clickwrap/requestGET /public-client/v1/clickwrap/contentGET /public-client/v1/clickwrap/customizationsPOST /public-client/v1/clickwrap-bundle/requestGET /public-client/v1/clickwrap-bundle/customizations- the Clickterm CDN
/sdk/clickterm-widget-translations.jsontranslation request
Each eligible call gets at most two SDK-level attempts. Every attempt has a fresh five-second timeout. Before the second attempt, the SDK waits one second plus random jitter from zero to one second.
The SDK retries uncancelled transport and timeout failures, plus HTTP 408, 500, 502, 503, and 504. HTTP 429 is eligible only when Retry-After is a valid delta-seconds value or HTTP date resolving to zero through five seconds; the normal fixed backoff still applies. Cancellation, identifiable TLS/certificate failures, malformed requests, decoding failures, other 4xx responses, and other 5xx responses fail without retry. When an internal caller supplies a cancellation signal, cancellation also interrupts an active attempt or its backoff. The public dialog and inline loading calls do not expose a cancellation API, and no modal or inline handle exists until loading settles, so their active attempts remain bounded by the per-attempt timeout.
Agreement acceptance/decline (POST /public-client/v1/clickwrap and POST /public-client/v1/clickwrap-bundle) remains a one-shot request to avoid duplicate submissions. Browser-managed font, logo, and agreement-image loads are outside SDK retry handling. A customer backend verification call such as POST /clickwrap/verify is also outside this browser SDK and must use the customer's own transport policy.
Bundle tests
npm test -- --run tests/bundle-flow.spec.ts tests/dialog-callback-isolation.spec.tsbundle-flow.spec.ts covers structured errors, accepted and unverified rows,
step navigation, Decline All, accessibility names, customizations, and retry
states. dialog-callback-isolation.spec.ts verifies callback ordering and that a
host callback failure does not change the SDK result.
Local development
npm install
npm run startThen open http://127.0.0.1:3000/ for the dialog demo or http://127.0.0.1:3000/inline.html for the inline demo. The Vite config also exposes a /cors-proxy?url=... endpoint so the demos can hit the Clickterm API directly from localhost.
Note on hot reload. If rollup's watch doesn't pick up changes reliably, this workaround works:
npx nodemon --watch src --ext ts --exec "npm run start". No project-side changes needed.
Scripts
npm run start— runswatch:dev(rollup) andvitein parallel. Dev bundle written topublic/dist/index.js.npm run build— production build. Emits UMD, ESM, CJS, and.d.tsbundles underdist/.npm run build:dev— one-shot dev bundle intopublic/dist/index.js.npm run watch:dev— rollup in watch mode without starting Vite.npm run types— emit.d.tsfiles only.
Project layout
- src/index.ts — public exports (
ClicktermClient,ClicktermDialog,ClicktermDom). - src/widget.ts — orchestrates the clickwrap lifecycle (fetch template → render → submit agreement).
- src/client.ts —
HttpServicewrapping the Clickterm/public-client/v1/clickwrapendpoints. - src/inline/ — inline-mode registry and handle implementations.
- src/translations.ts — loads dialog translations from the environment-specific
cdn.clickterm.comdev or production SDK path.
Release
- Bump
versionin package.json. - Commit and push to
main. - Create a matching
v*git tag (e.g.v2.3.0) and push it. - Ensure the npm package
@clickterm/widgethas this GitHub repository configured as a trusted publisher.
The publish.yml workflow runs on any v* tag: it builds and publishes the package to npm using trusted publishing with --provenance. No NPM_TOKEN repo secret is required.
