feat: add Product Carousel message support

- Add ProductCarouselCard and ProductCarouselMessageOptions types
- Add productCarousel to AnyRegularMessageContent union type
- Create generateProductCarouselMessage function with validation
- Add productCarousel support in generateWAMessageContent
- Uses viewOnceMessage wrapper for iOS/Android compatibility
- Requires WhatsApp Business account with configured catalog

Usage:
await sock.sendMessage(jid, {
  productCarousel: {
    catalogId: '123456789',
    products: [
      { productId: 'produto_001' },
      { productId: 'produto_002' }
    ]
  },
  body: 'Confira nossos produtos!'
})

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Renato Alcara
2026-01-26 17:43:35 -03:00
parent 8c8e228525
commit 3d4fa3e007
2 changed files with 152 additions and 1 deletions
+62
View File
@@ -613,6 +613,45 @@ export type AlbumSendResult = {
totalLatencyMs: number
}
// ========== Product Carousel Message Types ==========
/**
* Single product card in a product carousel
* References a product from WhatsApp Business catalog
*/
export type ProductCarouselCard = {
/** Product retailer ID from the catalog */
productId: string
}
/**
* Options for generating a product carousel message
* Uses products from WhatsApp Business catalog
*
* @example
* ```typescript
* await sock.sendMessage(jid, {
* productCarousel: {
* catalogId: '123456789',
* products: [
* { productId: 'iphone_15' },
* { productId: 'macbook_air' },
* { productId: 'apple_watch' }
* ]
* },
* body: 'Check out our featured products!'
* })
* ```
*/
export type ProductCarouselMessageOptions = {
/** Catalog ID from WhatsApp Business */
catalogId: string
/** Products to display (2-10 cards required) */
products: ProductCarouselCard[]
/** Body text for the message */
body?: string
}
export type AnyRegularMessageContent = (
| ({
text: string
@@ -747,6 +786,29 @@ export type AnyRegularMessageContent = (
title?: string
footer?: string
}
| {
/**
* Product Carousel Message - Swipeable product cards from WhatsApp Business catalog
* Requires: WhatsApp Business account with configured catalog
*
* @example
* ```typescript
* await sock.sendMessage(jid, {
* productCarousel: {
* catalogId: '123456789',
* products: [
* { productId: 'produto_001' },
* { productId: 'produto_002' },
* { productId: 'produto_003' }
* ]
* },
* body: 'Confira nossos produtos em destaque!'
* })
* ```
*/
productCarousel: ProductCarouselMessageOptions
body?: string
}
| {
/**
* Album message - send multiple images/videos grouped together
+90 -1
View File
@@ -25,6 +25,7 @@ import type {
MessageWithContextInfo,
NativeButton,
NativeFlowButton,
ProductCarouselMessageOptions,
ProductListMessageOptions,
WAMediaUpload,
WAMessage,
@@ -883,6 +884,82 @@ export const generateProductListMessage = (options: ProductListMessageOptions):
}
}
/**
* Generates a product carousel message using products from WhatsApp Business catalog
* Uses viewOnceMessage wrapper for better iOS/Android compatibility
*
* @example
* ```typescript
* const msg = generateProductCarouselMessage({
* catalogId: '123456789',
* products: [
* { productId: 'produto_001' },
* { productId: 'produto_002' },
* { productId: 'produto_003' }
* ],
* body: 'Confira nossos produtos em destaque!'
* })
* await sock.sendMessage(jid, msg)
* ```
*/
export const generateProductCarouselMessage = (
options: ProductCarouselMessageOptions
): WAMessageContent => {
const { catalogId, products, body } = options
if (!catalogId || typeof catalogId !== 'string' || catalogId.trim().length === 0) {
throw new Boom('catalogId is required and must be a non-empty string', { statusCode: 400 })
}
if (!products || products.length < 2) {
throw new Boom('Product carousel requires at least 2 products', { statusCode: 400 })
}
if (products.length > 10) {
throw new Boom('Maximum 10 products allowed in carousel', { statusCode: 400 })
}
// Validate each product has a valid productId
for (let i = 0; i < products.length; i++) {
const product = products[i]!
if (!product.productId || typeof product.productId !== 'string' || product.productId.trim().length === 0) {
throw new Boom(`Product at index ${i} must have a non-empty productId`, { statusCode: 400 })
}
}
// Build cards array with product references
const cards = products.map((product, index) => ({
card_index: index,
type: 'product' as const,
action: {
product_retailer_id: product.productId,
catalog_id: catalogId
}
}))
// Build the interactive message with carousel type
const interactiveMessage: proto.Message.IInteractiveMessage = {
body: { text: body || '' },
carouselMessage: {
cards: cards as any,
messageVersion: 1
}
}
// Wrap in viewOnceMessage for better compatibility
return {
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2
},
interactiveMessage
}
}
}
}
// ========== Legacy Message Functions ==========
/**
@@ -1003,7 +1080,7 @@ export const generateWAMessageContent = async (
m.viewOnceMessage = generated.viewOnceMessage
options.logger?.info('Sending listMessage with viewOnceMessage wrapper')
}
// Check for productList (multi-product message from catalog)
// Check for productList (multi-product message from catalog)
else if (hasNonNullishProperty(message, 'productList')) {
const productMsg = message as any
const productListOptions: ProductListMessageOptions = {
@@ -1019,6 +1096,18 @@ export const generateWAMessageContent = async (
m.listMessage = generated.listMessage
options.logger?.info('Sending productListMessage (multi-product from catalog)')
}
// Check for productCarousel (WhatsApp Business catalog products)
else if (hasNonNullishProperty(message, 'productCarousel')) {
const productCarouselMsg = message as any
const productCarouselOptions: ProductCarouselMessageOptions = {
catalogId: productCarouselMsg.productCarousel.catalogId,
products: productCarouselMsg.productCarousel.products,
body: productCarouselMsg.body
}
const generated = generateProductCarouselMessage(productCarouselOptions)
m.viewOnceMessage = generated.viewOnceMessage
options.logger?.info('Sending productCarouselMessage with viewOnceMessage wrapper')
}
// ⚠️ EXPERIMENTAL: Check for interactive messages FIRST (buttons, lists, templates)
// These use the older API which may not work reliably
else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) {