Skip to main content

GL Product Detail

Version: 1.0.12
Package: @gift-card-market/gl-product-detail
Last Updated: June 12, 2026

A reusable React product detail component for Gift Card Market. This package exposes a React component (GlProductDetail) that renders a product/preview UI and provides programmatic control via a React ref and named exports. It is built in TypeScript and produces ESM/CJS/IIFE bundles for consumption in different environments.

This document describes how to develop, build, and consume the package.

Table of Contents


Features

  • Display detailed information about a product before adding it to the cart or choosing to purchase. This includes product information (product name, product images, rating, view count, address, phone number, product description) and additional required information that the user needs to provide (amount, delivery method, recipient information, sender information, message, delivery date) before deciding to select the product and proceed to the next step.
  • Choose whether to show or hide the amount section, the custom amount button, the delivery method section, and the footer. You can also choose whether the action button is a "Buy Now" button, an "Add to Cart" button, or show both.
  • Customize the list of amounts, processing fee, minimum card amount, button labels, and footer text.
  • Programmatic control via React ref (ProductDetailFormHandle) or via top-level named exports (setAmount, triggerBuy, etc.) from anywhere.
  • Supports embedding as a Web Component in non-React environments.
  • Written in TypeScript, styled with Tailwind CSS (via PostCSS pipeline).

Requirements

  • React: ^18.0.0 or ^19.0.0.
  • Node.js: Version 18+ is recommended.

Installation

This package is published under the @gift-card-market scope as a private package. Configure your package manager to access the registry, then install.

1. Configure Registry & Authentication

Add the following to your project's .npmrc file:

@gift-card-market:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN

Alternatively, login via the command line:

npm login --registry=https://npm.pkg.github.com --scope=@gift-card-market

2. Install the Package

Using npm:

npm install @gift-card-market/gl-product-detail

Using pnpm:

pnpm add @gift-card-market/gl-product-detail

3. Import CSS

To apply the styling, import the compiled CSS file in your app's entry file:

import '@gift-card-market/gl-product-detail/dist/style.css';

Quick Start with React

Complete Usage Example

Below is a complete, interactive React example showing how to mount GlProductDetail, bind to the ProductDetailFormHandle ref, configure options (including custom amounts and event callbacks), register DOM event listeners, and trigger API methods programmatically.

import React, { useEffect, useRef, useState } from 'react';
import { GlProductDetail } from '@gift-card-market/gl-product-detail';
import type { ProductDetailFormHandle } from '@gift-card-market/gl-product-detail';
import '@gift-card-market/gl-product-detail/dist/style.css';

export default function MyComponent() {
const ref = useRef<ProductDetailFormHandle | null>(null);
const [isAmount, setIsAmount] = useState(false);
const [isSetFieldError, setIsSetFieldError] = useState(false);
const [hasResponse, setHasResponse] = useState(false);
const [selectedMethod, setSelectedMethod] = useState<string>('');

const methods = ['refresh', 'reset', 'setAmount', 'triggerBuy', 'triggerAddToCart', 'clearErrors', 'setFieldError'];

const data = {
jwt: '{jwt-token}',
businessId: '{businessId}',
environment: 'development', // 'development' | 'production'
themeColor: '#0052cc',
configuration: {
displayPrice: true,
displayCustomAmount: true,
displayDelivery: true,
displayDecsription: true, // Note: Typed as displayDecsription due to a typo in the SDK code
minCardAmount: 25,
feeAmount: 10,
denominations: '25,50,100,150', // Comma-separated string of card values
ctaType: 'buy', // 'buy' for Buy Now only, 'cart' for Add to Cart only, empty to show both
ctbaText: 'Buy',
ctcaText: 'Add To Cart',
ctbaUrl: null,
ctcaUrl: null,
footerText: '<strong>Thank you for your purchase!</strong>',
showFooter: false
}
};

const onSetAmount = async () => {
const amountInput = (document.getElementById('input-amount') as HTMLInputElement);
const amount = parseInt(amountInput.value, 10);
if (isNaN(amount)) return alert('Please enter a valid amount');
await ref.current?.setAmount(amount);
};

const onSetFieldError = async () => {
const fieldInput = (document.getElementById('input-field') as HTMLInputElement);
const messageInput = (document.getElementById('input-message') as HTMLInputElement);
const field = fieldInput.value;
const message = messageInput.value;
if (!field || !message) return alert('Please enter both field and message');
await ref.current?.setFieldError(field, message);
};

const handleProductDetailLoadedEvent = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('Product Detail Loaded Event Received:', detail);
};

const handleBuyClickedEvent = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('Product Detail Buy Clicked Event Received:', detail);
};

const handleCartClickedEvent = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('Product Detail Cart Clicked Event Received:', detail);
};

const handleAmountChangedEvent = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('Product Detail Amount Changed Event Received:', detail);
};

const handleErrorEvent = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('Error Event Received:', detail);
};

const handleResponse = (response: any) => {
const responseArea = document.getElementById('response-area') as HTMLTextAreaElement | null;
if (responseArea) responseArea.value = JSON.stringify(response, null, 2);
};

const triggerEvent = async () => {
if (!selectedMethod) return alert('Please select a method');

switch (selectedMethod) {
case 'reset':
await ref.current?.reset();
break;
case 'refresh':
await ref.current?.refresh();
break;
case 'setAmount':
await onSetAmount();
break;
case 'triggerBuy':
const submitResponse = await ref.current?.triggerBuy();
handleResponse(submitResponse);
break;
case 'triggerAddToCart':
const cartResponse = await ref.current?.triggerAddToCart();
handleResponse(cartResponse);
break;
case 'clearErrors':
await ref.current?.clearErrors();
break;
case 'setFieldError':
await onSetFieldError();
break;
default:
break;
}
};

useEffect(() => {
document.addEventListener('gl:pdp-loaded', handleProductDetailLoadedEvent);
document.addEventListener('gl:buy-clicked', handleBuyClickedEvent);
document.addEventListener('gl:cart-clicked', handleCartClickedEvent);
document.addEventListener('gl:amount-changed', handleAmountChangedEvent);
document.addEventListener('gl:error', handleErrorEvent);

return () => {
document.removeEventListener('gl:pdp-loaded', handleProductDetailLoadedEvent);
document.removeEventListener('gl:buy-clicked', handleBuyClickedEvent);
document.removeEventListener('gl:cart-clicked', handleCartClickedEvent);
document.removeEventListener('gl:amount-changed', handleAmountChangedEvent);
document.removeEventListener('gl:error', handleErrorEvent);
};
}, []);

return (
<div>
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
<div className="w-full max-w-3xl grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="w-full">
<select
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
onChange={(e) => {
const method = (e.target as HTMLSelectElement).value;
switch (method) {
case 'setAmount':
setIsAmount(true);
setIsSetFieldError(false);
setHasResponse(false);
break;
case 'setFieldError':
setIsAmount(false);
setIsSetFieldError(true);
setHasResponse(false);
break;
case 'triggerBuy':
case 'triggerAddToCart':
setIsAmount(false);
setIsSetFieldError(false);
setHasResponse(true);
break;
default:
setIsAmount(false);
setIsSetFieldError(false);
setHasResponse(false);
break;
}
setSelectedMethod(method);
}}
>
<option value="">-- Select Method --</option>
{methods.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</div>

<div className="w-full py-2">
{isAmount && (
<input
id="input-amount"
placeholder="Enter amount"
type="number"
inputMode="numeric"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
)}
{isSetFieldError && (
<input
id="input-field"
placeholder="Enter field"
type="text"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
)}
{isSetFieldError && (
<input
id="input-message"
placeholder="Enter message"
type="text"
className="w-full p-3 mt-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
)}
{hasResponse && (
<textarea
id="response-area"
disabled
className="w-full h-20 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
)}
</div>

<div className="w-full flex items-end">
<button
className="w-full py-3 font-semibold bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 select-none rounded-lg"
onClick={triggerEvent}
>
Run Method
</button>
</div>
</div>
</div>

<GlProductDetail ref={ref} data={data} />
</div>
);
}

Named Exports (Without Ref)

If you need to trigger component methods from outside the React tree, you can import and call these methods directly. They proxy directly to the currently mounted component instance:

import { reset, refresh, setAmount, triggerBuy, triggerAddToCart, clearErrors, setFieldError } from '@gift-card-market/gl-product-detail';

reset();
refresh();
setAmount(50);
const buyResponse = await triggerBuy();
const cartResponse = await triggerAddToCart();
clearErrors();
setFieldError('amount', 'Amount must be at least $10');

Note: If no instance of the component is mounted, these functions are safe no-ops.


Detailed API

Props GlProductDetailProps

The GlProductDetail component expects a single data prop with the following interface:

PropTypeRequiredDescription
jwtstringJWT for calling Gift Card Market API. Access the OAuth2 Authentication Guide to see instructions for obtaining a token.
businessIdstringUnique identifier of the business/merchant to display.
environmentstringBackend environment (e.g. 'development', 'production'). Default is 'production'.
themeColorstringThe primary theme color (e.g. '#0052cc'). Defaults to component theme if not provided.
configurationRecord<string, any>Component-specific customization options. If not provided, defaults will be used.

ConfigurationDefined

The type interface defining all available options in data.configuration:

export interface ConfigurationDefined {
displayPrice?: boolean; // Show or hide the price/amount selector (default: true)
displayDelivery?: boolean; // Show or hide delivery methods/recipient forms (default: true)
displayCustomAmount?: boolean; // Enable custom amount input field (default: false)
displayDecsription?: boolean; // Show/hide business short description (default: true, note the SDK typo 'displayDecsription')
minCardAmount?: number; // Minimum custom amount allowed (default: 25)
feeAmount?: number; // Processing fee per card (default: 0)
denominations?: string; // Comma-separated card value presets (default: '25,50,100')
ctaType?: string; // 'buy' for Buy Now, 'cart' for Add to Cart, empty for both
ctbaText?: string; // Text label for the Buy Now button (default: 'Buy Now')
ctcaText?: string; // Text label for the Add to Cart button (default: 'Add to Cart')
ctbaUrl?: string | null; // Redirect URL for Buy Now (default: null, triggers programmatic event)
ctcaUrl?: string | null; // Redirect URL for Add to Cart (default: null, triggers programmatic event)
showFooter?: boolean; // Show or hide footer section (default: true)
footerText?: string; // HTML string content to render in footer
}

ConfigurationProps (Defaults)

If not explicitly defined in data.configuration, the component uses the following default values:

PropertyDefault ValueDescription
displayPricetruePrice/amount section is displayed.
displayDeliverytrueDelivery options and recipient details form are displayed.
displayCustomAmountfalseCustom amount input field is hidden.
displayDecsriptiontrueBusiness description is displayed (note typo).
minCardAmount25Minimum customizable gift card value is $25.
feeAmount0No transaction processing fee.
denominations"25,50,100"Default presets options are $25, $50, and $100.
ctaType"" (empty)Shows both the "Buy Now" and "Add to Cart" buttons.
ctbaText"Buy Now"Label on the Buy Now button.
ctcaText"Add to Cart"Label on the Add to Cart button.
ctbaUrlnullNo redirect; fires custom DOM event on click.
ctcaUrlnullNo redirect; fires custom DOM event on click.
showFootertrueFooter section is displayed.
footerText""No custom footer text (uses default layout).

ProductDetailFormHandle (ref API)

The following methods are exposed via the React ref object:

  • reset(): void
    Resets the internal component state (selected amount, form errors, fields) back to defaults.
  • refresh(): void
    Re-fetches business configuration and card details from the API and re-renders.
  • setAmount(amount: number): void
    Programmatically updates the selected gift card value.
  • triggerBuy(): Promise<object>
    Triggers validation and the purchase flow. Returns the submission payload.
  • triggerAddToCart(): Promise<object>
    Triggers validation and the add-to-cart flow. Returns the submission payload.
  • clearErrors(): void
    Clears any validation errors currently shown in the form UI.
  • setFieldError(field: string, message: string): void
    Manually sets a validation error message for a specific form field.

ProductDetailEvents (Event bus)

The component fires custom DOM events on document to let the parent application react to user interactions:

  • gl:pdp-loaded
    Fires when the PDP finishes loading. event.detail contains the loaded product data.
  • gl:buy-clicked
    Fires when the user clicks the Buy Now button. event.detail contains form data.
  • gl:cart-clicked
    Fires when the user clicks the Add to Cart button. event.detail contains form data.
  • gl:amount-changed
    Fires when the active card value changes. event.detail contains the new amount.
  • gl:error
    Fires when a validation or API request fails. event.detail contains the error details.

Build & Publish

Build scripts are defined in package.json. The package uses tsup to compile typescript files into ESM, CJS, and IIFE formats, and Tailwind CLI to build the CSS:

  • Build CSS and JS:
    npm run build
    # or
    pnpm run build

This outputs built modules to the dist/ directory and compiles the stylesheet to dist/style.css.


Troubleshooting

dist/style.css not generated

The build script runs Tailwind CLI (npx tailwindcss -i ./src/styles.css -o ./dist/style.css --minify). Ensure tailwindcss is installed in devDependencies and the command runs in the package directory. If you see errors about PostCSS config being an ES module, rename postcss.config.js to postcss.config.cjs or update the package type field or the PostCSS loader.

PostCSS / Tailwind errors (e.g. unknown utilities, @apply issues)

Check src/styles.css and remove @apply usages for classes that may not be available. Verify tailwind.config.js content globs include the package's src files so Tailwind can find used classes.

SASS/Dart deprecation warnings

These are informational for Dart Sass's legacy API. Keep sass updated and check any plugin that uses legacy API.

esbuild / loader errors for .scss during CJS/ESM build

Ensure bundler configs (tsup/vite) include appropriate loaders or prebuild steps for CSS/SCSS. The package currently uses Tailwind CLI to compile CSS separately.


File Layout

Key files in the package directory structure:

src/
├─ gl-product-detail.tsx # main React component
├─ index.ts # package entry point (exports helper functions)
├─ lib/controller.ts # controller instance and method proxies
├─ types/ # TypeScript definitions (form-handle, product, etc.)
└─ components/ # internal UI subcomponents
demo/ # Vite-based demo app