@kuralle-agents/messaging-meta 0.7.2 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/webhook/normalizer.d.ts +12 -0
- package/dist/webhook/normalizer.js +2 -0
- package/dist/whatsapp/client.d.ts +41 -5
- package/dist/whatsapp/client.js +139 -4
- package/dist/whatsapp/commerce.d.ts +58 -0
- package/dist/whatsapp/commerce.js +88 -0
- package/dist/whatsapp/index.d.ts +2 -1
- package/dist/whatsapp/index.js +2 -0
- package/dist/whatsapp/types.d.ts +167 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @kuralle-agents/messaging-meta @kuralle-agents/messaging
|
|
|
12
12
|
|
|
13
13
|
Provides production-ready clients for Meta's three messaging platforms, built on a shared Graph API foundation with retry logic, rate limiting, and unified error handling.
|
|
14
14
|
|
|
15
|
-
- **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages, CTA buttons, WhatsApp Flows, reactions, locations, contacts.
|
|
15
|
+
- **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages, CTA buttons, WhatsApp Flows, reactions, locations, contacts, commerce (single/multi-product, catalog, and address messages, plus inbound order parsing).
|
|
16
16
|
- **Messenger** — button templates, generic templates (carousel), quick replies, sender actions, user profile lookups.
|
|
17
17
|
- **Instagram** — text (auto-split at 1000 bytes), quick replies, carousels, private replies to comments, ice breakers, message tags.
|
|
18
18
|
- **Main barrel** — `GraphAPIClient`, `BaseMetaClient`, `verifySignature`, `normalizeWebhook`, error classes.
|
|
@@ -66,6 +66,35 @@ const instagram = createInstagramClient({
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### WhatsApp commerce
|
|
70
|
+
|
|
71
|
+
Share catalog products and receive orders without leaving WhatsApp:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { parseInboundOrder } from '@kuralle-agents/messaging-meta/whatsapp';
|
|
75
|
+
|
|
76
|
+
// Single product, multi-product (max 10 sections / 30 products), catalog, address request
|
|
77
|
+
await whatsapp.sendProduct(to, { catalogId, productRetailerId: 'sku-1', body: { text: 'Check this out' } });
|
|
78
|
+
await whatsapp.sendProductList(to, {
|
|
79
|
+
header: { type: 'text', text: 'Bestsellers' },
|
|
80
|
+
body: { text: 'Pick your favorites' },
|
|
81
|
+
catalogId,
|
|
82
|
+
sections: [{ title: 'Cakes', productRetailerIds: ['sku-1', 'sku-2'] }],
|
|
83
|
+
});
|
|
84
|
+
await whatsapp.sendCatalog(to, { body: { text: 'Browse our catalog' } });
|
|
85
|
+
await whatsapp.sendAddressRequest(to, { body: { text: 'Where should we deliver?' }, country: 'IN' });
|
|
86
|
+
|
|
87
|
+
// Inbound orders (webhook message type "order") arrive typed:
|
|
88
|
+
whatsapp.onMessage(async (msg) => {
|
|
89
|
+
const order = parseInboundOrder(msg);
|
|
90
|
+
if (order) {
|
|
91
|
+
// order.catalog_id, order.product_items[].{product_retailer_id, quantity, item_price, currency}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Address submissions arrive as `nfm_reply` interactive messages — use `parseInboundAddress(msg)` for a typed accessor. Address messages are country-gated by Meta (currently India only).
|
|
97
|
+
|
|
69
98
|
### Webhook verification without a client
|
|
70
99
|
|
|
71
100
|
```typescript
|
|
@@ -81,7 +110,7 @@ const events = normalizeWebhook(JSON.parse(rawBody));
|
|
|
81
110
|
| Import path | Contents |
|
|
82
111
|
|---|---|
|
|
83
112
|
| `@kuralle-agents/messaging-meta` | `GraphAPIClient`, `BaseMetaClient`, errors, `verifySignature`, `normalizeWebhook`, `MessengerClient`, `InstagramClient` |
|
|
84
|
-
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, format converter |
|
|
113
|
+
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, commerce (`parseInboundOrder`, `parseInboundAddress`), format converter |
|
|
85
114
|
| `@kuralle-agents/messaging-meta/messenger` | `MessengerClient`, `createMessengerClient`, format converter |
|
|
86
115
|
| `@kuralle-agents/messaging-meta/instagram` | `InstagramClient`, `createInstagramClient`, ice breakers, format converter |
|
|
87
116
|
| `@kuralle-agents/messaging-meta/webhooks` | `verifySignature`, `normalizeWebhook` only |
|
|
@@ -83,12 +83,24 @@ export interface NormalizedMessage {
|
|
|
83
83
|
nfm_reply?: {
|
|
84
84
|
name?: string;
|
|
85
85
|
response_json: string;
|
|
86
|
+
body?: string;
|
|
86
87
|
};
|
|
87
88
|
};
|
|
88
89
|
button?: {
|
|
89
90
|
text: string;
|
|
90
91
|
payload: string;
|
|
91
92
|
};
|
|
93
|
+
/** Order placed from a catalog, single-, or multi-product message. */
|
|
94
|
+
order?: {
|
|
95
|
+
catalog_id: string;
|
|
96
|
+
text?: string;
|
|
97
|
+
product_items: Array<{
|
|
98
|
+
product_retailer_id: string;
|
|
99
|
+
quantity: number;
|
|
100
|
+
item_price: number;
|
|
101
|
+
currency: string;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
92
104
|
/** Quoted/replied-to message context. */
|
|
93
105
|
context?: {
|
|
94
106
|
message_id: string;
|
|
@@ -128,6 +128,8 @@ function normalizeWhatsAppWebhook(payload) {
|
|
|
128
128
|
normalized.interactive = msg.interactive;
|
|
129
129
|
if (msg.button)
|
|
130
130
|
normalized.button = msg.button;
|
|
131
|
+
if (msg.order)
|
|
132
|
+
normalized.order = msg.order;
|
|
131
133
|
if (msg.context)
|
|
132
134
|
normalized.context = msg.context;
|
|
133
135
|
if (msg.referral)
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Extends {@link BaseMetaClient} for shared webhook verification, payload
|
|
7
7
|
* normalization, handler dispatch, and `/webhook` Hono sub-app. Templates,
|
|
8
|
-
* Flows, phone-number management, CTA buttons,
|
|
9
|
-
*
|
|
8
|
+
* Flows, phone-number management, CTA buttons, commerce (product, catalog,
|
|
9
|
+
* and address messages), reactions, location, and contact messages remain
|
|
10
|
+
* in this file as WhatsApp-specific surface.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```ts
|
|
@@ -24,7 +25,7 @@ import { type FormatConverter, type InboundMessage, type InteractiveMessage, typ
|
|
|
24
25
|
import { BaseMetaClient } from '../base-client.js';
|
|
25
26
|
import type { BaseMetaClientConfig } from '../base-client.js';
|
|
26
27
|
import type { NormalizedMessage, NormalizedReaction, NormalizedStatus } from '../webhook/normalizer.js';
|
|
27
|
-
import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
|
|
28
|
+
import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, CatalogMessage, AddressMessage, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
|
|
28
29
|
type WhatsAppInbound = NormalizedMessage;
|
|
29
30
|
type WhatsAppOutbound = Record<string, unknown>;
|
|
30
31
|
/** Internal config — base-client common fields threaded through the public config. */
|
|
@@ -34,8 +35,8 @@ export declare function createWhatsAppClient(config: WhatsAppClientConfig): What
|
|
|
34
35
|
* Full-featured WhatsApp Cloud API client.
|
|
35
36
|
*
|
|
36
37
|
* Implements the `PlatformClient` interface via {@link BaseMetaClient}.
|
|
37
|
-
* Layers WhatsApp-specific capabilities (templates, Flows,
|
|
38
|
-
* buttons, locations, contacts) on top.
|
|
38
|
+
* Layers WhatsApp-specific capabilities (templates, Flows, commerce,
|
|
39
|
+
* reactions, CTA buttons, locations, contacts) on top.
|
|
39
40
|
*/
|
|
40
41
|
export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, WhatsAppOutbound, InternalWhatsAppConfig> {
|
|
41
42
|
readonly platform: "whatsapp";
|
|
@@ -76,6 +77,40 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
|
|
|
76
77
|
sendInteractiveButtons(to: string, msg: ButtonMessage): Promise<SendResult>;
|
|
77
78
|
sendCTAButton(to: string, cta: CTAButtonMessage): Promise<SendResult>;
|
|
78
79
|
sendInteractiveFlow(to: string, flow: FlowInteractiveInput): Promise<SendResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Send a single-product message (`interactive.type: "product"`).
|
|
82
|
+
*
|
|
83
|
+
* Displays one catalog product with a **View** button; the user can open
|
|
84
|
+
* the Product Detail Page, add to cart, and send an order. Orders arrive
|
|
85
|
+
* as inbound `type: "order"` messages — see {@link parseInboundOrder}.
|
|
86
|
+
*/
|
|
87
|
+
sendProduct(to: string, product: ProductMessage): Promise<SendResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Send a multi-product message (`interactive.type: "product_list"`).
|
|
90
|
+
*
|
|
91
|
+
* Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
|
|
92
|
+
* up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
|
|
93
|
+
* required by the Cloud API.
|
|
94
|
+
*
|
|
95
|
+
* @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
|
|
96
|
+
* product limits are violated, or a section has no products.
|
|
97
|
+
*/
|
|
98
|
+
sendProductList(to: string, list: ProductListMessage): Promise<SendResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Send a catalog message (`interactive.type: "catalog_message"`).
|
|
101
|
+
*
|
|
102
|
+
* Displays a product thumbnail header, custom body text, and a
|
|
103
|
+
* **View catalog** button that opens the full catalog within WhatsApp.
|
|
104
|
+
*/
|
|
105
|
+
sendCatalog(to: string, catalog: CatalogMessage): Promise<SendResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Send an address request message (`interactive.type: "address_message"`).
|
|
108
|
+
*
|
|
109
|
+
* Country-gated by Meta — currently India-based businesses and their
|
|
110
|
+
* India customers only. The user's submission arrives as an inbound
|
|
111
|
+
* `nfm_reply` interactive message — see {@link parseInboundAddress}.
|
|
112
|
+
*/
|
|
113
|
+
sendAddressRequest(to: string, address: AddressMessage): Promise<SendResult>;
|
|
79
114
|
sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
|
|
80
115
|
sendLocation(to: string, location: LocationPayload): Promise<SendResult>;
|
|
81
116
|
sendContacts(to: string, contacts: ContactPayload[]): Promise<SendResult>;
|
|
@@ -103,6 +138,7 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
|
|
|
103
138
|
protected toInboundMessage(msg: NormalizedMessage): InboundMessage;
|
|
104
139
|
protected toStatusUpdate(status: NormalizedStatus): StatusUpdate;
|
|
105
140
|
protected toReactionData(reaction: NormalizedReaction): ReactionData;
|
|
141
|
+
private validateProductList;
|
|
106
142
|
private toSendResult;
|
|
107
143
|
private mapMessageType;
|
|
108
144
|
private extractTextFallback;
|
package/dist/whatsapp/client.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Extends {@link BaseMetaClient} for shared webhook verification, payload
|
|
7
7
|
* normalization, handler dispatch, and `/webhook` Hono sub-app. Templates,
|
|
8
|
-
* Flows, phone-number management, CTA buttons,
|
|
9
|
-
*
|
|
8
|
+
* Flows, phone-number management, CTA buttons, commerce (product, catalog,
|
|
9
|
+
* and address messages), reactions, location, and contact messages remain
|
|
10
|
+
* in this file as WhatsApp-specific surface.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```ts
|
|
@@ -24,6 +25,7 @@ import { MessagingError, WindowClosedError, FileHashDedupStrategy, UploadStrateg
|
|
|
24
25
|
import { BaseMetaClient } from '../base-client.js';
|
|
25
26
|
import { GraphAPIClient } from '../graph-api/client.js';
|
|
26
27
|
import { SmartSplitter } from '../message-splitter.js';
|
|
28
|
+
import { MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
|
27
29
|
import { WhatsAppFormatConverter } from './format.js';
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
29
31
|
// Factory
|
|
@@ -38,8 +40,8 @@ export function createWhatsAppClient(config) {
|
|
|
38
40
|
* Full-featured WhatsApp Cloud API client.
|
|
39
41
|
*
|
|
40
42
|
* Implements the `PlatformClient` interface via {@link BaseMetaClient}.
|
|
41
|
-
* Layers WhatsApp-specific capabilities (templates, Flows,
|
|
42
|
-
* buttons, locations, contacts) on top.
|
|
43
|
+
* Layers WhatsApp-specific capabilities (templates, Flows, commerce,
|
|
44
|
+
* reactions, CTA buttons, locations, contacts) on top.
|
|
43
45
|
*/
|
|
44
46
|
export class WhatsAppClient extends BaseMetaClient {
|
|
45
47
|
platform = 'whatsapp';
|
|
@@ -281,6 +283,124 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
281
283
|
return this.toSendResult(to, response);
|
|
282
284
|
}
|
|
283
285
|
// =========================================================================
|
|
286
|
+
// WhatsApp-specific — Commerce (products, catalog, addresses)
|
|
287
|
+
// =========================================================================
|
|
288
|
+
/**
|
|
289
|
+
* Send a single-product message (`interactive.type: "product"`).
|
|
290
|
+
*
|
|
291
|
+
* Displays one catalog product with a **View** button; the user can open
|
|
292
|
+
* the Product Detail Page, add to cart, and send an order. Orders arrive
|
|
293
|
+
* as inbound `type: "order"` messages — see {@link parseInboundOrder}.
|
|
294
|
+
*/
|
|
295
|
+
async sendProduct(to, product) {
|
|
296
|
+
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
297
|
+
messaging_product: 'whatsapp',
|
|
298
|
+
recipient_type: 'individual',
|
|
299
|
+
to,
|
|
300
|
+
type: 'interactive',
|
|
301
|
+
interactive: {
|
|
302
|
+
type: 'product',
|
|
303
|
+
body: product.body,
|
|
304
|
+
footer: product.footer,
|
|
305
|
+
action: {
|
|
306
|
+
catalog_id: product.catalogId,
|
|
307
|
+
product_retailer_id: product.productRetailerId,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
return this.toSendResult(to, response);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Send a multi-product message (`interactive.type: "product_list"`).
|
|
315
|
+
*
|
|
316
|
+
* Displays up to {@link MAX_PRODUCT_LIST_PRODUCTS} catalog products across
|
|
317
|
+
* up to {@link MAX_PRODUCT_LIST_SECTIONS} sections. Header and body are
|
|
318
|
+
* required by the Cloud API.
|
|
319
|
+
*
|
|
320
|
+
* @throws {MessagingError} `INVALID_PRODUCT_LIST` when the section or
|
|
321
|
+
* product limits are violated, or a section has no products.
|
|
322
|
+
*/
|
|
323
|
+
async sendProductList(to, list) {
|
|
324
|
+
this.validateProductList(list);
|
|
325
|
+
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
326
|
+
messaging_product: 'whatsapp',
|
|
327
|
+
recipient_type: 'individual',
|
|
328
|
+
to,
|
|
329
|
+
type: 'interactive',
|
|
330
|
+
interactive: {
|
|
331
|
+
type: 'product_list',
|
|
332
|
+
header: list.header,
|
|
333
|
+
body: list.body,
|
|
334
|
+
footer: list.footer,
|
|
335
|
+
action: {
|
|
336
|
+
catalog_id: list.catalogId,
|
|
337
|
+
sections: list.sections.map((section) => ({
|
|
338
|
+
title: section.title,
|
|
339
|
+
product_items: section.productRetailerIds.map((id) => ({
|
|
340
|
+
product_retailer_id: id,
|
|
341
|
+
})),
|
|
342
|
+
})),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
return this.toSendResult(to, response);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Send a catalog message (`interactive.type: "catalog_message"`).
|
|
350
|
+
*
|
|
351
|
+
* Displays a product thumbnail header, custom body text, and a
|
|
352
|
+
* **View catalog** button that opens the full catalog within WhatsApp.
|
|
353
|
+
*/
|
|
354
|
+
async sendCatalog(to, catalog) {
|
|
355
|
+
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
356
|
+
messaging_product: 'whatsapp',
|
|
357
|
+
recipient_type: 'individual',
|
|
358
|
+
to,
|
|
359
|
+
type: 'interactive',
|
|
360
|
+
interactive: {
|
|
361
|
+
type: 'catalog_message',
|
|
362
|
+
body: catalog.body,
|
|
363
|
+
footer: catalog.footer,
|
|
364
|
+
action: {
|
|
365
|
+
name: 'catalog_message',
|
|
366
|
+
parameters: catalog.thumbnailProductRetailerId
|
|
367
|
+
? { thumbnail_product_retailer_id: catalog.thumbnailProductRetailerId }
|
|
368
|
+
: undefined,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
return this.toSendResult(to, response);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Send an address request message (`interactive.type: "address_message"`).
|
|
376
|
+
*
|
|
377
|
+
* Country-gated by Meta — currently India-based businesses and their
|
|
378
|
+
* India customers only. The user's submission arrives as an inbound
|
|
379
|
+
* `nfm_reply` interactive message — see {@link parseInboundAddress}.
|
|
380
|
+
*/
|
|
381
|
+
async sendAddressRequest(to, address) {
|
|
382
|
+
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
383
|
+
messaging_product: 'whatsapp',
|
|
384
|
+
recipient_type: 'individual',
|
|
385
|
+
to,
|
|
386
|
+
type: 'interactive',
|
|
387
|
+
interactive: {
|
|
388
|
+
type: 'address_message',
|
|
389
|
+
body: address.body,
|
|
390
|
+
action: {
|
|
391
|
+
name: 'address_message',
|
|
392
|
+
parameters: {
|
|
393
|
+
country: address.country,
|
|
394
|
+
values: address.values,
|
|
395
|
+
saved_addresses: address.savedAddresses,
|
|
396
|
+
validation_errors: address.validationErrors,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
return this.toSendResult(to, response);
|
|
402
|
+
}
|
|
403
|
+
// =========================================================================
|
|
284
404
|
// WhatsApp-specific — Reactions, location, contacts
|
|
285
405
|
// =========================================================================
|
|
286
406
|
async sendReaction(to, messageId, emoji) {
|
|
@@ -490,6 +610,19 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
490
610
|
// =========================================================================
|
|
491
611
|
// Private — conversion helpers
|
|
492
612
|
// =========================================================================
|
|
613
|
+
validateProductList(list) {
|
|
614
|
+
if (list.sections.length === 0 || list.sections.length > MAX_PRODUCT_LIST_SECTIONS) {
|
|
615
|
+
throw new MessagingError(`Product list must have between 1 and ${MAX_PRODUCT_LIST_SECTIONS} sections (has ${list.sections.length})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
|
|
616
|
+
}
|
|
617
|
+
const emptySection = list.sections.find((s) => s.productRetailerIds.length === 0);
|
|
618
|
+
if (emptySection) {
|
|
619
|
+
throw new MessagingError(`Product list section "${emptySection.title}" has no products`, 'INVALID_PRODUCT_LIST', 'whatsapp');
|
|
620
|
+
}
|
|
621
|
+
const totalProducts = list.sections.reduce((sum, s) => sum + s.productRetailerIds.length, 0);
|
|
622
|
+
if (totalProducts > MAX_PRODUCT_LIST_PRODUCTS) {
|
|
623
|
+
throw new MessagingError(`Product list exceeds maximum of ${MAX_PRODUCT_LIST_PRODUCTS} products across all sections (has ${totalProducts})`, 'INVALID_PRODUCT_LIST', 'whatsapp');
|
|
624
|
+
}
|
|
625
|
+
}
|
|
493
626
|
toSendResult(to, response) {
|
|
494
627
|
return {
|
|
495
628
|
messageId: response.messages[0]?.id ?? '',
|
|
@@ -527,6 +660,8 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
527
660
|
return msg.interactive.button_reply.title;
|
|
528
661
|
if (msg.interactive?.list_reply)
|
|
529
662
|
return msg.interactive.list_reply.title;
|
|
663
|
+
if (msg.order?.text)
|
|
664
|
+
return msg.order.text;
|
|
530
665
|
if (msg.location) {
|
|
531
666
|
return msg.location.name ?? `${msg.location.latitude},${msg.location.longitude}`;
|
|
532
667
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module whatsapp/commerce
|
|
3
|
+
*
|
|
4
|
+
* Helpers and limits for WhatsApp commerce messages.
|
|
5
|
+
*
|
|
6
|
+
* Commerce messages let you share products from a Meta catalog and receive
|
|
7
|
+
* orders directly in WhatsApp. The send methods live on the
|
|
8
|
+
* {@link WhatsAppClient} (`sendProduct`, `sendProductList`, `sendCatalog`,
|
|
9
|
+
* `sendAddressRequest`); this module provides the inbound typed accessors.
|
|
10
|
+
*
|
|
11
|
+
* @see https://developers.facebook.com/documentation/business-messaging/whatsapp/catalogs/share-products
|
|
12
|
+
* @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
|
|
13
|
+
*/
|
|
14
|
+
import type { InboundMessage } from '@kuralle-agents/messaging';
|
|
15
|
+
import type { WhatsAppInboundOrder, WhatsAppInboundAddress } from './types.js';
|
|
16
|
+
export type { ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, } from './types.js';
|
|
17
|
+
/** Maximum number of sections in a multi-product message. */
|
|
18
|
+
export declare const MAX_PRODUCT_LIST_SECTIONS = 10;
|
|
19
|
+
/** Maximum number of products across all sections of a multi-product message. */
|
|
20
|
+
export declare const MAX_PRODUCT_LIST_PRODUCTS = 30;
|
|
21
|
+
/**
|
|
22
|
+
* Extract the typed order from an inbound message, when the user placed an
|
|
23
|
+
* order from a catalog, single-, or multi-product message.
|
|
24
|
+
*
|
|
25
|
+
* The generic `InboundMessage` type has no `order` field, so the order
|
|
26
|
+
* travels on `message.raw` (the normalized WhatsApp webhook message). This
|
|
27
|
+
* accessor returns it typed.
|
|
28
|
+
*
|
|
29
|
+
* @param message - An inbound message from the WhatsApp client.
|
|
30
|
+
* @returns The order, or `undefined` if the message is not an order.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* client.onMessage(async (msg) => {
|
|
35
|
+
* const order = parseInboundOrder(msg);
|
|
36
|
+
* if (order) {
|
|
37
|
+
* console.log(`Order against catalog ${order.catalog_id}:`);
|
|
38
|
+
* for (const item of order.product_items) {
|
|
39
|
+
* console.log(` ${item.quantity}x ${item.product_retailer_id} @ ${item.item_price} ${item.currency}`);
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseInboundOrder(message: InboundMessage): WhatsAppInboundOrder | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Extract the typed address submission from an inbound message, when the
|
|
48
|
+
* user replied to an address message (`sendAddressRequest`).
|
|
49
|
+
*
|
|
50
|
+
* Address replies arrive as `interactive.type === "nfm_reply"` messages with
|
|
51
|
+
* `nfm_reply.name === "address_message"`; the address fields are JSON-encoded
|
|
52
|
+
* in `nfm_reply.response_json`.
|
|
53
|
+
*
|
|
54
|
+
* @param message - An inbound message from the WhatsApp client.
|
|
55
|
+
* @returns The parsed address, or `undefined` if the message is not an
|
|
56
|
+
* address submission (or the payload is malformed).
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseInboundAddress(message: InboundMessage): WhatsAppInboundAddress | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module whatsapp/commerce
|
|
3
|
+
*
|
|
4
|
+
* Helpers and limits for WhatsApp commerce messages.
|
|
5
|
+
*
|
|
6
|
+
* Commerce messages let you share products from a Meta catalog and receive
|
|
7
|
+
* orders directly in WhatsApp. The send methods live on the
|
|
8
|
+
* {@link WhatsAppClient} (`sendProduct`, `sendProductList`, `sendCatalog`,
|
|
9
|
+
* `sendAddressRequest`); this module provides the inbound typed accessors.
|
|
10
|
+
*
|
|
11
|
+
* @see https://developers.facebook.com/documentation/business-messaging/whatsapp/catalogs/share-products
|
|
12
|
+
* @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
|
|
13
|
+
*/
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Limits (per Meta Cloud API docs)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/** Maximum number of sections in a multi-product message. */
|
|
18
|
+
export const MAX_PRODUCT_LIST_SECTIONS = 10;
|
|
19
|
+
/** Maximum number of products across all sections of a multi-product message. */
|
|
20
|
+
export const MAX_PRODUCT_LIST_PRODUCTS = 30;
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Inbound typed accessors
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Extract the typed order from an inbound message, when the user placed an
|
|
26
|
+
* order from a catalog, single-, or multi-product message.
|
|
27
|
+
*
|
|
28
|
+
* The generic `InboundMessage` type has no `order` field, so the order
|
|
29
|
+
* travels on `message.raw` (the normalized WhatsApp webhook message). This
|
|
30
|
+
* accessor returns it typed.
|
|
31
|
+
*
|
|
32
|
+
* @param message - An inbound message from the WhatsApp client.
|
|
33
|
+
* @returns The order, or `undefined` if the message is not an order.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* client.onMessage(async (msg) => {
|
|
38
|
+
* const order = parseInboundOrder(msg);
|
|
39
|
+
* if (order) {
|
|
40
|
+
* console.log(`Order against catalog ${order.catalog_id}:`);
|
|
41
|
+
* for (const item of order.product_items) {
|
|
42
|
+
* console.log(` ${item.quantity}x ${item.product_retailer_id} @ ${item.item_price} ${item.currency}`);
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function parseInboundOrder(message) {
|
|
49
|
+
const raw = message.raw;
|
|
50
|
+
const order = raw?.order;
|
|
51
|
+
if (!order || typeof order.catalog_id !== 'string' || !Array.isArray(order.product_items)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return order;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract the typed address submission from an inbound message, when the
|
|
58
|
+
* user replied to an address message (`sendAddressRequest`).
|
|
59
|
+
*
|
|
60
|
+
* Address replies arrive as `interactive.type === "nfm_reply"` messages with
|
|
61
|
+
* `nfm_reply.name === "address_message"`; the address fields are JSON-encoded
|
|
62
|
+
* in `nfm_reply.response_json`.
|
|
63
|
+
*
|
|
64
|
+
* @param message - An inbound message from the WhatsApp client.
|
|
65
|
+
* @returns The parsed address, or `undefined` if the message is not an
|
|
66
|
+
* address submission (or the payload is malformed).
|
|
67
|
+
*/
|
|
68
|
+
export function parseInboundAddress(message) {
|
|
69
|
+
const raw = message.raw;
|
|
70
|
+
const nfm = raw?.interactive?.nfm_reply;
|
|
71
|
+
if (!nfm || nfm.name !== 'address_message' || !nfm.response_json) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(nfm.response_json);
|
|
76
|
+
if (parsed !== null &&
|
|
77
|
+
typeof parsed === 'object' &&
|
|
78
|
+
!Array.isArray(parsed) &&
|
|
79
|
+
typeof parsed.values === 'object' &&
|
|
80
|
+
parsed.values !== null) {
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/whatsapp/index.d.ts
CHANGED
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
* @packageDocumentation
|
|
31
31
|
*/
|
|
32
32
|
export { WhatsAppClient, createWhatsAppClient } from './client.js';
|
|
33
|
-
export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
|
|
33
|
+
export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
|
|
34
34
|
export { buildTemplateSendPayload, buildTemplatePayload, mapOutboundTemplateComponents, } from './templates.js';
|
|
35
35
|
export type { TypedTemplateConfig, RawTemplateComponents, } from './templates.js';
|
|
36
36
|
export { WhatsAppFormatConverter } from './format.js';
|
|
37
37
|
export { splitMessage } from './split.js';
|
|
38
38
|
export { generateFlowToken, buildFlowInput } from './flows.js';
|
|
39
|
+
export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
package/dist/whatsapp/index.js
CHANGED
|
@@ -39,3 +39,5 @@ export { WhatsAppFormatConverter } from './format.js';
|
|
|
39
39
|
export { splitMessage } from './split.js';
|
|
40
40
|
// ─── Flows ──────────────────────────────────────────────────────────────
|
|
41
41
|
export { generateFlowToken, buildFlowInput } from './flows.js';
|
|
42
|
+
// ─── Commerce ───────────────────────────────────────────────────────────
|
|
43
|
+
export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
package/dist/whatsapp/types.d.ts
CHANGED
|
@@ -279,6 +279,173 @@ export interface FlowInteractiveInput {
|
|
|
279
279
|
/** Optional initial data for the flow. */
|
|
280
280
|
flowActionPayload?: Record<string, unknown>;
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* A single-product interactive message (`interactive.type: "product"`).
|
|
284
|
+
*
|
|
285
|
+
* Displays one product from your catalog with a product image, title,
|
|
286
|
+
* price, and a **View** button that opens the Product Detail Page.
|
|
287
|
+
*/
|
|
288
|
+
export interface ProductMessage {
|
|
289
|
+
/** Optional body text (max 1024 chars). */
|
|
290
|
+
body?: {
|
|
291
|
+
text: string;
|
|
292
|
+
};
|
|
293
|
+
/** Optional footer text (max 60 chars). */
|
|
294
|
+
footer?: {
|
|
295
|
+
text: string;
|
|
296
|
+
};
|
|
297
|
+
/** Meta product catalog ID. */
|
|
298
|
+
catalogId: string;
|
|
299
|
+
/** Product SKU (labeled **Content ID** in Commerce Manager). */
|
|
300
|
+
productRetailerId: string;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A multi-product interactive message (`interactive.type: "product_list"`).
|
|
304
|
+
*
|
|
305
|
+
* Displays up to 30 products from your catalog, organized into up to
|
|
306
|
+
* 10 sections. Header and body are required by the Cloud API.
|
|
307
|
+
*/
|
|
308
|
+
export interface ProductListMessage {
|
|
309
|
+
/** Required header (text only for product lists). */
|
|
310
|
+
header: {
|
|
311
|
+
type: 'text';
|
|
312
|
+
text: string;
|
|
313
|
+
};
|
|
314
|
+
/** Body text (required, max 1024 chars). */
|
|
315
|
+
body: {
|
|
316
|
+
text: string;
|
|
317
|
+
};
|
|
318
|
+
/** Optional footer text (max 60 chars). */
|
|
319
|
+
footer?: {
|
|
320
|
+
text: string;
|
|
321
|
+
};
|
|
322
|
+
/** Meta product catalog ID. */
|
|
323
|
+
catalogId: string;
|
|
324
|
+
/** Product sections (max 10; max 30 products across all sections). */
|
|
325
|
+
sections: ProductSection[];
|
|
326
|
+
}
|
|
327
|
+
/** A section within a multi-product message. */
|
|
328
|
+
export interface ProductSection {
|
|
329
|
+
/** Section title. */
|
|
330
|
+
title: string;
|
|
331
|
+
/** Product SKUs in this section (Content IDs in Commerce Manager). */
|
|
332
|
+
productRetailerIds: string[];
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* A catalog interactive message (`interactive.type: "catalog_message"`).
|
|
336
|
+
*
|
|
337
|
+
* Displays a product thumbnail header image, custom body text, and a
|
|
338
|
+
* **View catalog** button that opens your full catalog within WhatsApp.
|
|
339
|
+
*/
|
|
340
|
+
export interface CatalogMessage {
|
|
341
|
+
/** Body text (required, max 1024 chars). */
|
|
342
|
+
body: {
|
|
343
|
+
text: string;
|
|
344
|
+
};
|
|
345
|
+
/** Optional footer text (max 60 chars). */
|
|
346
|
+
footer?: {
|
|
347
|
+
text: string;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Optional product SKU whose image is used as the header thumbnail.
|
|
351
|
+
* When omitted, the first item in the catalog is used.
|
|
352
|
+
*/
|
|
353
|
+
thumbnailProductRetailerId?: string;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Address field values used in address messages.
|
|
357
|
+
*
|
|
358
|
+
* Field names mirror the Cloud API wire format. Currently only available
|
|
359
|
+
* for India-based businesses and their India customers.
|
|
360
|
+
*/
|
|
361
|
+
export interface WhatsAppAddressValues {
|
|
362
|
+
/** Recipient name. */
|
|
363
|
+
name?: string;
|
|
364
|
+
/** Recipient phone number. */
|
|
365
|
+
phone_number?: string;
|
|
366
|
+
/** Pin code (India, max 6 chars). */
|
|
367
|
+
in_pin_code?: string;
|
|
368
|
+
/** Flat/house number. */
|
|
369
|
+
house_number?: string;
|
|
370
|
+
/** Floor number. */
|
|
371
|
+
floor_number?: string;
|
|
372
|
+
/** Tower number. */
|
|
373
|
+
tower_number?: string;
|
|
374
|
+
/** Building/apartment name. */
|
|
375
|
+
building_name?: string;
|
|
376
|
+
/** Street address. */
|
|
377
|
+
address?: string;
|
|
378
|
+
/** Landmark/area. */
|
|
379
|
+
landmark_area?: string;
|
|
380
|
+
/** City. */
|
|
381
|
+
city?: string;
|
|
382
|
+
/** State. */
|
|
383
|
+
state?: string;
|
|
384
|
+
}
|
|
385
|
+
/** A previously saved address offered to the user in an address message. */
|
|
386
|
+
export interface WhatsAppSavedAddress {
|
|
387
|
+
/** Identifier returned as `saved_address_id` when the user selects it. */
|
|
388
|
+
id: string;
|
|
389
|
+
/** The saved address field values. */
|
|
390
|
+
value: WhatsAppAddressValues;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* An address request interactive message (`interactive.type: "address_message"`).
|
|
394
|
+
*
|
|
395
|
+
* Country-gated: only available for India-based businesses and their
|
|
396
|
+
* India customers. The user's submission arrives as an inbound
|
|
397
|
+
* `interactive.type: "nfm_reply"` message — see {@link parseInboundAddress}.
|
|
398
|
+
*/
|
|
399
|
+
export interface AddressMessage {
|
|
400
|
+
/** Body text (required). */
|
|
401
|
+
body: {
|
|
402
|
+
text: string;
|
|
403
|
+
};
|
|
404
|
+
/** ISO country code (required by the API, e.g. `"IN"`). */
|
|
405
|
+
country: string;
|
|
406
|
+
/** Optional prefilled address field values. */
|
|
407
|
+
values?: WhatsAppAddressValues;
|
|
408
|
+
/** Optional saved addresses the user can pick instead of typing. */
|
|
409
|
+
savedAddresses?: WhatsAppSavedAddress[];
|
|
410
|
+
/**
|
|
411
|
+
* Optional per-field validation errors. WhatsApp blocks submission
|
|
412
|
+
* until the flagged fields are corrected.
|
|
413
|
+
*/
|
|
414
|
+
validationErrors?: Partial<Record<keyof WhatsAppAddressValues, string>>;
|
|
415
|
+
}
|
|
416
|
+
/** A line item in an inbound WhatsApp order (wire format). */
|
|
417
|
+
export interface WhatsAppOrderItem {
|
|
418
|
+
/** Product SKU (Content ID in Commerce Manager). */
|
|
419
|
+
product_retailer_id: string;
|
|
420
|
+
/** Quantity ordered. */
|
|
421
|
+
quantity: number;
|
|
422
|
+
/** Unit price of the item. */
|
|
423
|
+
item_price: number;
|
|
424
|
+
/** Catalog currency code (e.g. `"USD"`). */
|
|
425
|
+
currency: string;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* An inbound order placed by a user from a catalog, single-, or
|
|
429
|
+
* multi-product message (webhook message `type: "order"`).
|
|
430
|
+
*/
|
|
431
|
+
export interface WhatsAppInboundOrder {
|
|
432
|
+
/** Meta product catalog ID the order was placed against. */
|
|
433
|
+
catalog_id: string;
|
|
434
|
+
/** Text accompanying the order. */
|
|
435
|
+
text?: string;
|
|
436
|
+
/** Ordered line items. */
|
|
437
|
+
product_items: WhatsAppOrderItem[];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* A parsed inbound address submission (the `nfm_reply.response_json`
|
|
441
|
+
* of an address message reply).
|
|
442
|
+
*/
|
|
443
|
+
export interface WhatsAppInboundAddress {
|
|
444
|
+
/** ID of the saved address the user selected, when applicable. */
|
|
445
|
+
saved_address_id?: string;
|
|
446
|
+
/** Address fields entered or confirmed by the user. */
|
|
447
|
+
values: WhatsAppAddressValues;
|
|
448
|
+
}
|
|
282
449
|
/** A location payload for sending a map pin. */
|
|
283
450
|
export interface LocationPayload {
|
|
284
451
|
/** Latitude in decimal degrees. */
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-messaging-meta"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.8.5",
|
|
10
10
|
"description": "Meta platform clients (WhatsApp, Messenger, Instagram) for Kuralle messaging",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/index.js",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@kuralle-agents/http-client": "0.
|
|
45
|
-
"@kuralle-agents/messaging": "0.
|
|
44
|
+
"@kuralle-agents/http-client": "0.8.5",
|
|
45
|
+
"@kuralle-agents/messaging": "0.8.5"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"hono": "^4.12.0"
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"redis": "^4.7.0",
|
|
59
59
|
"typescript": "^5.8.2",
|
|
60
60
|
"zod": "^4.0.0",
|
|
61
|
-
"@kuralle-agents/
|
|
62
|
-
"@kuralle-agents/
|
|
63
|
-
"@kuralle-agents/engagement": "0.
|
|
61
|
+
"@kuralle-agents/core": "0.8.5",
|
|
62
|
+
"@kuralle-agents/hono-server": "0.8.5",
|
|
63
|
+
"@kuralle-agents/engagement": "0.8.5"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"prebuild": "rm -rf dist",
|