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
- Requirements
- Installation
- Quick Start with React
- Detailed API
- Build & Publish
- Troubleshooting
- File Layout
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.0or^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:
| Prop | Type | Required | Description |
|---|---|---|---|
jwt | string | ✅ | JWT for calling Gift Card Market API. Access the OAuth2 Authentication Guide to see instructions for obtaining a token. |
businessId | string | ✅ | Unique identifier of the business/merchant to display. |
environment | string | ❌ | Backend environment (e.g. 'development', 'production'). Default is 'production'. |
themeColor | string | ❌ | The primary theme color (e.g. '#0052cc'). Defaults to component theme if not provided. |
configuration | Record<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:
| Property | Default Value | Description |
|---|---|---|
displayPrice | true | Price/amount section is displayed. |
displayDelivery | true | Delivery options and recipient details form are displayed. |
displayCustomAmount | false | Custom amount input field is hidden. |
displayDecsription | true | Business description is displayed (note typo). |
minCardAmount | 25 | Minimum customizable gift card value is $25. |
feeAmount | 0 | No 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. |
ctbaUrl | null | No redirect; fires custom DOM event on click. |
ctcaUrl | null | No redirect; fires custom DOM event on click. |
showFooter | true | Footer 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.detailcontains the loaded product data.gl:buy-clicked
Fires when the user clicks the Buy Now button.event.detailcontains form data.gl:cart-clicked
Fires when the user clicks the Add to Cart button.event.detailcontains form data.gl:amount-changed
Fires when the active card value changes.event.detailcontains the new amount.gl:error
Fires when a validation or API request fails.event.detailcontains 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