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:
@@ -613,6 +613,45 @@ export type AlbumSendResult = {
|
|||||||
totalLatencyMs: number
|
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 = (
|
export type AnyRegularMessageContent = (
|
||||||
| ({
|
| ({
|
||||||
text: string
|
text: string
|
||||||
@@ -747,6 +786,29 @@ export type AnyRegularMessageContent = (
|
|||||||
title?: string
|
title?: string
|
||||||
footer?: 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
|
* Album message - send multiple images/videos grouped together
|
||||||
|
|||||||
+90
-1
@@ -25,6 +25,7 @@ import type {
|
|||||||
MessageWithContextInfo,
|
MessageWithContextInfo,
|
||||||
NativeButton,
|
NativeButton,
|
||||||
NativeFlowButton,
|
NativeFlowButton,
|
||||||
|
ProductCarouselMessageOptions,
|
||||||
ProductListMessageOptions,
|
ProductListMessageOptions,
|
||||||
WAMediaUpload,
|
WAMediaUpload,
|
||||||
WAMessage,
|
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 ==========
|
// ========== Legacy Message Functions ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1003,7 +1080,7 @@ export const generateWAMessageContent = async (
|
|||||||
m.viewOnceMessage = generated.viewOnceMessage
|
m.viewOnceMessage = generated.viewOnceMessage
|
||||||
options.logger?.info('Sending listMessage with viewOnceMessage wrapper')
|
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')) {
|
else if (hasNonNullishProperty(message, 'productList')) {
|
||||||
const productMsg = message as any
|
const productMsg = message as any
|
||||||
const productListOptions: ProductListMessageOptions = {
|
const productListOptions: ProductListMessageOptions = {
|
||||||
@@ -1019,6 +1096,18 @@ export const generateWAMessageContent = async (
|
|||||||
m.listMessage = generated.listMessage
|
m.listMessage = generated.listMessage
|
||||||
options.logger?.info('Sending productListMessage (multi-product from catalog)')
|
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)
|
// ⚠️ EXPERIMENTAL: Check for interactive messages FIRST (buttons, lists, templates)
|
||||||
// These use the older API which may not work reliably
|
// These use the older API which may not work reliably
|
||||||
else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) {
|
else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user