flow_chat 0.9.0 → 0.10.0

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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +43 -0
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +166 -295
  5. data/Rakefile +12 -1
  6. data/SECURITY.md +1 -1
  7. data/docs/architecture.md +52 -479
  8. data/docs/async-background-processing.md +31 -265
  9. data/docs/configuration.md +106 -613
  10. data/docs/factory-pattern.md +27 -324
  11. data/docs/gateway-context-variables.md +140 -143
  12. data/docs/gateway-development.md +86 -650
  13. data/docs/getting-started.md +40 -379
  14. data/docs/instrumentation.md +88 -279
  15. data/docs/platforms/instagram.md +278 -0
  16. data/docs/platforms/messenger.md +205 -0
  17. data/docs/platforms/telegram.md +47 -951
  18. data/docs/platforms/ussd.md +38 -653
  19. data/docs/platforms/whatsapp.md +73 -1321
  20. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  21. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  22. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  23. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  24. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  25. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  26. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  27. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  28. data/docs/testing.md +26 -297
  29. data/examples/http_controller.rb +12 -10
  30. data/examples/intercom_configuration_example.rb +19 -24
  31. data/examples/intercom_controller.rb +8 -20
  32. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  33. data/examples/simulator_controller.rb +0 -1
  34. data/examples/ussd_controller.rb +86 -158
  35. data/examples/whatsapp_controller.rb +16 -15
  36. data/examples/whatsapp_media_examples.rb +27 -79
  37. data/lib/flow_chat/app.rb +129 -11
  38. data/lib/flow_chat/choice_titles.rb +95 -0
  39. data/lib/flow_chat/config.rb +124 -1
  40. data/lib/flow_chat/delivery_error.rb +9 -0
  41. data/lib/flow_chat/http/configuration_error.rb +9 -0
  42. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  43. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  44. data/lib/flow_chat/input.rb +86 -0
  45. data/lib/flow_chat/instagram/client.rb +32 -0
  46. data/lib/flow_chat/instagram/configuration.rb +147 -0
  47. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  48. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  49. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  50. data/lib/flow_chat/instagram/renderer.rb +23 -0
  51. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  52. data/lib/flow_chat/instrumentation.rb +160 -1
  53. data/lib/flow_chat/intercom/client.rb +34 -28
  54. data/lib/flow_chat/intercom/configuration.rb +2 -49
  55. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  56. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  57. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  58. data/lib/flow_chat/intercom/renderer.rb +57 -5
  59. data/lib/flow_chat/media.rb +121 -0
  60. data/lib/flow_chat/messenger/client.rb +264 -0
  61. data/lib/flow_chat/messenger/configuration.rb +103 -0
  62. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  63. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  64. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  65. data/lib/flow_chat/messenger/renderer.rb +150 -0
  66. data/lib/flow_chat/meta/challenge.rb +24 -0
  67. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  68. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  69. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  70. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  71. data/lib/flow_chat/meta/signature.rb +30 -0
  72. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  73. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  74. data/lib/flow_chat/named_configuration.rb +65 -0
  75. data/lib/flow_chat/prompt.rb +13 -16
  76. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  77. data/lib/flow_chat/security.rb +76 -0
  78. data/lib/flow_chat/session/middleware.rb +11 -2
  79. data/lib/flow_chat/simulator/controller.rb +31 -15
  80. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  81. data/lib/flow_chat/telegram/client.rb +47 -4
  82. data/lib/flow_chat/telegram/configuration.rb +2 -42
  83. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  84. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  85. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  86. data/lib/flow_chat/telegram/renderer.rb +10 -2
  87. data/lib/flow_chat/text_truncator.rb +75 -0
  88. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  89. data/lib/flow_chat/version.rb +1 -1
  90. data/lib/flow_chat/whatsapp/client.rb +14 -7
  91. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  92. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  93. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
  94. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  95. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  96. data/lib/flow_chat.rb +0 -10
  97. data/site/.nojekyll +0 -0
  98. data/site/.og-card.html +89 -0
  99. data/site/favicon.svg +6 -0
  100. data/site/index.html +209 -0
  101. data/site/og.png +0 -0
  102. metadata +51 -3
  103. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
@@ -1,22 +1,23 @@
1
- # WhatsApp Development Guide
1
+ # WhatsApp
2
2
 
3
- FlowChat provides comprehensive WhatsApp Business API integration with support for rich media, interactive elements, secure webhook validation, and flexible deployment modes.
3
+ The `FlowChat::Whatsapp::Gateway::CloudApi` gateway integrates the WhatsApp Business Cloud API. It handles Meta's webhook verification and signature checks, parses inbound messages (text, interactive replies, media, location, contacts), and renders your flow's output as WhatsApp interactive messages.
4
4
 
5
- ## ✨ Key Features
5
+ ## Credentials
6
6
 
7
- - **Rich Media Support**: Images, documents, audio, video, and stickers
8
- - **Interactive Elements**: Buttons (up to 3), lists (up to 10 items per section)
9
- - **Secure Webhooks**: HMAC-SHA256 signature validation
10
- - **Multiple Processing Modes**: Inline, background, and simulator
11
- - **Media Upload & Download**: Direct file upload and media handling
12
- - **Multi-Tenant Support**: Named configurations for different accounts
13
- - **Development Tools**: Built-in simulator for testing flows
7
+ The gateway needs an access token, a phone number id, and a verify token; an app secret is needed to validate webhook signatures. Provide them through Rails credentials, environment variables, or a configuration object.
14
8
 
15
- ## 🚀 Quick Start
9
+ ```yaml
10
+ # config/credentials.yml.enc
11
+ whatsapp:
12
+ access_token: "..."
13
+ phone_number_id: "..."
14
+ verify_token: "..." # your own value, echoed back during webhook setup
15
+ app_secret: "..." # used to verify X-Hub-Signature-256
16
+ ```
16
17
 
17
- FlowChat supports two WhatsApp gateway options:
18
+ Equivalent environment variables: `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_BUSINESS_ACCOUNT_ID`.
18
19
 
19
- ### Option 1: Meta Cloud API (Recommended)
20
+ ## Setup
20
21
 
21
22
  ```ruby
22
23
  # app/controllers/whatsapp_controller.rb
@@ -29,1367 +30,118 @@ class WhatsappController < ApplicationController
29
30
  config.use_session_store FlowChat::Session::CacheSessionStore
30
31
  end
31
32
 
32
- processor.run WelcomeFlow, :main_page
33
- rescue => e
34
- Rails.logger.error "WhatsApp webhook error: #{e.message}"
35
- head :internal_server_error
36
- end
37
- end
38
- ```
39
-
40
- ### Option 2: Twilio WhatsApp API
41
-
42
- ```ruby
43
- # app/controllers/whatsapp_twilio_controller.rb
44
- class WhatsappTwilioController < ApplicationController
45
- skip_forgery_protection
46
-
47
- def webhook
48
- processor = FlowChat::Processor.new(self) do |config|
49
- config.use_gateway FlowChat::Whatsapp::Gateway::Twilio
50
- config.use_session_store FlowChat::Session::CacheSessionStore
51
- end
52
-
53
- processor.run WelcomeFlow, :main_page
54
- rescue => e
55
- Rails.logger.error "Twilio WhatsApp webhook error: #{e.message}"
56
- head :internal_server_error
33
+ processor.run RegistrationFlow, :main_page
57
34
  end
58
35
  end
59
36
  ```
60
37
 
61
- ### 2. Add Routes
62
-
63
38
  ```ruby
64
39
  # config/routes.rb
65
- post '/whatsapp/webhook', to: 'whatsapp#webhook' # Meta Cloud API
66
- post '/whatsapp/twilio/webhook', to: 'whatsapp_twilio#webhook' # Twilio API
67
- ```
68
-
69
- ### 3. Create a Flow
70
-
71
- ```ruby
72
- # app/flow_chat/welcome_flow.rb
73
- class WelcomeFlow < FlowChat::Flow
74
- def main_page
75
- name = app.screen(:name) do |prompt|
76
- prompt.ask "Hello! What's your name?",
77
- transform: ->(input) { input.strip.titleize }
78
- end
79
-
80
- choice = app.screen(:main_menu) do |prompt|
81
- prompt.select "Hi #{name}! How can I help?", {
82
- "info" => "📋 Get Information",
83
- "support" => "🆘 Contact Support",
84
- "feedback" => "💬 Give Feedback"
85
- }
86
- end
87
-
88
- case choice
89
- when "info"
90
- show_info
91
- when "support"
92
- contact_support
93
- when "feedback"
94
- collect_feedback
95
- end
96
- end
97
-
98
- private
99
-
100
- def show_info
101
- app.say "📍 Located at 123 Main Street\n🕒 Hours: Mon-Fri 9AM-6PM"
102
- end
103
-
104
- def contact_support
105
- app.say "📞 Call us at (555) 123-4567\n📧 Email: support@example.com"
106
- end
107
-
108
- def collect_feedback
109
- rating = app.screen(:rating) do |prompt|
110
- prompt.select "Rate our service:", ["⭐", "⭐⭐", "⭐⭐⭐", "⭐⭐⭐⭐", "⭐⭐⭐⭐⭐"]
111
- end
112
-
113
- app.say "Thank you for your #{rating} rating! 🙏"
114
- end
115
- end
116
- ```
117
-
118
- ## 🔧 Configuration
119
-
120
- ### Meta Cloud API Configuration
121
-
122
- #### Option 1: Rails Credentials (Recommended)
123
-
124
- ```bash
125
- rails credentials:edit
126
- ```
127
-
128
- ```yaml
129
- whatsapp:
130
- access_token: "your_access_token"
131
- phone_number_id: "your_phone_number_id"
132
- verify_token: "your_verify_token"
133
- app_id: "your_app_id"
134
- app_secret: "your_app_secret"
135
- business_account_id: "your_business_account_id"
136
- skip_signature_validation: false # Set to true only for development
137
- ```
138
-
139
- #### Option 2: Environment Variables
140
-
141
- ```bash
142
- export WHATSAPP_ACCESS_TOKEN="your_access_token"
143
- export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
144
- export WHATSAPP_VERIFY_TOKEN="your_verify_token"
145
- export WHATSAPP_APP_ID="your_app_id"
146
- export WHATSAPP_APP_SECRET="your_app_secret"
147
- export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
148
- export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
149
- ```
150
-
151
- ### Twilio WhatsApp Configuration
152
-
153
- #### Option 1: Rails Credentials (Recommended)
154
-
155
- ```bash
156
- rails credentials:edit
157
- ```
158
-
159
- ```yaml
160
- twilio_whatsapp:
161
- account_sid: "your_twilio_account_sid"
162
- auth_token: "your_twilio_auth_token"
163
- phone_number: "+15551234567" # Your Twilio WhatsApp number
164
- skip_signature_validation: false # Set to true only for development
165
- ```
166
-
167
- #### Option 2: Environment Variables
168
-
169
- ```bash
170
- export TWILIO_ACCOUNT_SID="your_twilio_account_sid"
171
- export TWILIO_AUTH_TOKEN="your_twilio_auth_token"
172
- export TWILIO_WHATSAPP_PHONE_NUMBER="+15551234567"
173
- export TWILIO_WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
174
- ```
175
-
176
- ### Gateway Comparison
177
-
178
- | Feature | Meta Cloud API | Twilio |
179
- |---------|----------------|--------|
180
- | **Interactive Elements** | Native buttons/lists | Text fallback |
181
- | **Webhook Format** | JSON | Form-encoded |
182
- | **Signature Validation** | X-Hub-Signature-256 (SHA256) | X-Twilio-Signature (SHA1) |
183
- | **Response Format** | JSON | TwiML XML |
184
- | **Setup Complexity** | Moderate (Meta Business) | Simple (Twilio account) |
185
- | **Features** | Full WhatsApp features | Basic messaging |
186
- | **Cost** | Per conversation | Per message |
187
-
188
- ### Option 3: Programmatic Configuration
189
-
190
- ```ruby
191
- # Meta Cloud API
192
- cloud_config = FlowChat::Whatsapp::Configuration.new(:my_account)
193
- cloud_config.access_token = "your_access_token"
194
- cloud_config.phone_number_id = "your_phone_number_id"
195
- cloud_config.verify_token = "your_verify_token"
196
- cloud_config.app_secret = "your_app_secret"
197
-
198
- processor = FlowChat::Processor.new(self) do |config|
199
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, cloud_config
200
- config.use_session_store FlowChat::Session::CacheSessionStore
201
- end
202
-
203
- # Twilio WhatsApp
204
- twilio_config = FlowChat::Whatsapp::TwilioConfiguration.new(:twilio_account)
205
- twilio_config.account_sid = "your_account_sid"
206
- twilio_config.auth_token = "your_auth_token"
207
- twilio_config.phone_number = "+15551234567"
208
-
209
- processor = FlowChat::Processor.new(self) do |config|
210
- config.use_gateway FlowChat::Whatsapp::Gateway::Twilio, twilio_config
211
- config.use_session_store FlowChat::Session::CacheSessionStore
212
- end
213
- ```
214
-
215
- ## 📱 Interactive Elements
216
-
217
- ### Buttons (Up to 3)
218
-
219
- FlowChat automatically uses buttons for 3 or fewer choices:
220
-
221
- ```ruby
222
- def main_menu
223
- choice = app.screen(:menu) do |prompt|
224
- prompt.select "What would you like to do?", {
225
- "balance" => "💰 Check Balance",
226
- "transfer" => "📤 Transfer Money",
227
- "history" => "📜 View History"
228
- }
229
- end
230
-
231
- handle_choice(choice)
232
- end
233
- ```
234
-
235
- ### Lists (4+ Items)
236
-
237
- FlowChat automatically uses interactive lists for more than 3 choices:
238
-
239
- ```ruby
240
- def product_menu
241
- product = app.screen(:products) do |prompt|
242
- prompt.select "Choose a product:", {
243
- "laptop" => "💻 Laptop - $999",
244
- "phone" => "📱 Smartphone - $599",
245
- "tablet" => "📱 Tablet - $399",
246
- "watch" => "⌚ Smartwatch - $299",
247
- "headphones" => "🎧 Headphones - $199"
248
- }
249
- end
250
-
251
- show_product_details(product)
252
- end
253
- ```
254
-
255
- Lists support:
256
- - Up to 10 items per section
257
- - Automatic pagination for more items
258
- - Title truncation (24 chars) with full description (72 chars)
259
-
260
- ## 🎨 Rich Media Support
261
-
262
- ### Sending Media in Flows
263
-
264
- ```ruby
265
- def product_showcase
266
- # Image with prompt
267
- feedback = app.screen(:product_feedback) do |prompt|
268
- prompt.ask "What do you think of this product?",
269
- media: {
270
- type: :image,
271
- url: "https://example.com/product.jpg"
272
- }
273
- end
274
-
275
- # Document response
276
- app.say "Thanks! Here's the product catalog:",
277
- media: {
278
- type: :document,
279
- url: "https://example.com/catalog.pdf",
280
- filename: "product_catalog.pdf"
281
- }
282
- end
283
-
284
- def video_tutorial
285
- app.say "Watch this tutorial:",
286
- media: {
287
- type: :video,
288
- url: "https://example.com/tutorial.mp4"
289
- }
290
- end
291
- ```
292
-
293
- ### Media Types Supported
294
-
295
- | Type | Usage | Options |
296
- |------|-------|---------|
297
- | `:image` | Photos, screenshots | `url`, `caption` |
298
- | `:document` | PDFs, docs, files | `url`, `caption`, `filename` |
299
- | `:audio` | Voice messages, music | `url`, `caption` |
300
- | `:video` | Video files | `url`, `caption` |
301
- | `:sticker` | Stickers/animated | `url` (no caption) |
302
-
303
- ### Buttons with Media Headers
304
-
305
- ```ruby
306
- def media_menu
307
- choice = app.screen(:options) do |prompt|
308
- prompt.select "Choose an option:",
309
- {
310
- "details" => "📋 More Details",
311
- "buy" => "🛒 Buy Now",
312
- "share" => "📤 Share"
313
- },
314
- media: {
315
- type: :image,
316
- url: "https://example.com/product.jpg"
317
- }
318
- end
319
- end
320
- ```
321
-
322
- ### Handling Incoming Media
323
-
324
- ```ruby
325
- def handle_media_upload
326
- if app.media
327
- media_type = app.media["type"]
328
- media_id = app.media["id"]
329
-
330
- case media_type
331
- when "image"
332
- app.say "Thanks for the image! Processing..."
333
- process_image(media_id)
334
- when "document"
335
- app.say "Document received. Reviewing..."
336
- process_document(media_id)
337
- when "audio"
338
- app.say "Got your voice message!"
339
- process_audio(media_id)
340
- end
341
- else
342
- app.say "Please send a photo of your receipt."
343
- end
344
- end
345
- ```
346
-
347
- ## 📤 WhatsApp Client API
348
-
349
- The `FlowChat::Whatsapp::Client` provides a comprehensive API for sending messages outside of flows and integrating with other systems.
350
-
351
- ### Client Initialization
352
-
353
- ```ruby
354
- # From credentials/environment variables
355
- config = FlowChat::Whatsapp::Configuration.from_credentials
356
- client = FlowChat::Whatsapp::Client.new(config)
357
-
358
- # Using named configuration
359
- config = FlowChat::Whatsapp::Configuration.get(:my_account)
360
- client = FlowChat::Whatsapp::Client.new(config)
361
- ```
362
-
363
- ### Text Messages
364
-
365
- ```ruby
366
- # Simple text message
367
- client.send_text("+1234567890", "Hello! How can I help you today?")
368
- ```
369
-
370
- ### Interactive Messages
371
-
372
- ```ruby
373
- # Interactive buttons (up to 3)
374
- buttons = [
375
- {id: "option1", title: "View Orders"},
376
- {id: "option2", title: "Track Package"},
377
- {id: "option3", title: "Contact Support"}
378
- ]
379
- client.send_buttons("+1234567890", "What would you like to do?", buttons)
380
-
381
- # Interactive lists (4+ options)
382
- sections = [
383
- {
384
- title: "Products",
385
- rows: [
386
- {id: "laptop", title: "💻 Laptop", description: "High-performance laptop - $999"},
387
- {id: "phone", title: "📱 Smartphone", description: "Latest model smartphone - $599"},
388
- {id: "tablet", title: "📱 Tablet", description: "10-inch tablet - $399"}
389
- ]
390
- },
391
- {
392
- title: "Accessories",
393
- rows: [
394
- {id: "headphones", title: "🎧 Headphones", description: "Wireless headphones - $199"},
395
- {id: "case", title: "📱 Phone Case", description: "Protective case - $29"}
396
- ]
397
- }
398
- ]
399
- client.send_list("+1234567890", "Choose a product:", sections, "Browse")
400
- ```
401
-
402
- ### Media Messages
403
-
404
- ```ruby
405
- # Send image
406
- client.send_image("+1234567890", "https://example.com/image.jpg", "Check out our new product!")
407
-
408
- # Send document
409
- client.send_document(
410
- "+1234567890",
411
- "https://example.com/catalog.pdf",
412
- "Here's our product catalog",
413
- "catalog.pdf" # optional filename
414
- )
415
-
416
- # Send video
417
- client.send_video("+1234567890", "https://example.com/tutorial.mp4", "Watch this tutorial")
418
-
419
- # Send audio
420
- client.send_audio("+1234567890", "https://example.com/message.mp3")
421
-
422
- # Send sticker (no caption support)
423
- client.send_sticker("+1234567890", "https://example.com/sticker.webp")
424
- ```
425
-
426
- ### Media Upload & Download
427
-
428
- ```ruby
429
- # Upload local file and get media ID
430
- media_id = client.upload_media("/path/to/file.pdf", "application/pdf", "document.pdf")
431
-
432
- # Upload from IO object
433
- File.open("/path/to/image.jpg", "rb") do |file|
434
- media_id = client.upload_media(file, "image/jpeg", "photo.jpg")
435
- end
436
-
437
- # Use uploaded media ID in messages
438
- client.send_document("+1234567890", media_id, "Here's your document")
439
-
440
- # Get media URL from ID
441
- media_url = client.get_media_url(media_id)
442
-
443
- # Download media content
444
- media_content = client.download_media(media_id)
445
- File.write("/tmp/downloaded_file", media_content, mode: "wb")
446
- ```
447
-
448
- ### Template Messages
449
-
450
- ```ruby
451
- # Send template message (for conversation initiation)
452
- components = [
453
- {
454
- type: "body",
455
- parameters: [
456
- {type: "text", text: "John Doe"},
457
- {type: "text", text: "ORD-12345"}
458
- ]
459
- }
460
- ]
461
-
462
- client.send_template(
463
- "+1234567890",
464
- "order_confirmation",
465
- components,
466
- "en_US"
467
- )
40
+ match "/whatsapp/webhook", to: "whatsapp#webhook", via: [:get, :post]
468
41
  ```
469
42
 
470
- ### Read Receipts & Typing Indicator
471
-
472
- Mark an inbound message as read and optionally show a typing indicator while your application processes a slow request (LLM call, lookup, etc.). WhatsApp Cloud API ties the typing indicator to the mark-as-read call — there is no separate typing endpoint.
473
-
474
- ```ruby
475
- client = FlowChat::Whatsapp::Client.new(config)
476
-
477
- # Just mark as read
478
- client.mark_as_read("wamid.ABC123")
479
-
480
- # Mark as read AND show "typing…" in the user's chat
481
- client.mark_as_read("wamid.ABC123", typing: true)
482
-
483
- # Convenience equivalent of the line above
484
- client.indicate_typing("wamid.ABC123")
485
- ```
43
+ Both verbs are needed: Meta sends a `GET` with `hub.mode=subscribe` to verify the endpoint (the gateway answers it using your `verify_token`), and `POST`s the actual messages. Each `POST` is checked against `X-Hub-Signature-256` using the app secret; a request with a bad signature is answered `200 OK` without processing, so Meta stops retrying it.
486
44
 
487
- The typing indicator auto-dismisses on the next outbound message or after ~25 seconds. There is **no stop-typing call** send the real reply and the indicator clears.
45
+ With no second argument, `use_gateway` loads credentials through `FlowChat::Whatsapp::Configuration.from_credentials`, which reads the Rails credentials or environment variables above. That is the setup shown here.
488
46
 
489
- `message_id` must be the id of an inbound user message; you cannot show a typing indicator without an associated inbound message.
47
+ ## Explicit and multi-tenant configuration
490
48
 
491
- ### Complete Service Example
49
+ To run more than one WhatsApp number, or to load credentials from somewhere other than Rails credentials, build a `FlowChat::Whatsapp::Configuration` and pass it as the second argument to `use_gateway`.
492
50
 
493
51
  ```ruby
494
- # Service for sending notifications
495
- class WhatsappNotificationService
496
- def initialize(config_name = :default)
497
- @config = FlowChat::Whatsapp::Configuration.get(config_name)
498
- @client = FlowChat::Whatsapp::Client.new(@config)
499
- end
500
-
501
- def send_order_confirmation(phone_number, order_id)
502
- # Send confirmation message
503
- @client.send_text(phone_number, "Order ##{order_id} confirmed! 🛍️")
504
-
505
- # Send invoice document
506
- @client.send_document(
507
- phone_number,
508
- "https://storage.example.com/invoices/#{order_id}.pdf",
509
- "Your invoice is ready",
510
- "invoice_#{order_id}.pdf"
511
- )
512
-
513
- # Send interactive buttons
514
- @client.send_buttons(phone_number, "What would you like to do next?", [
515
- {id: "track", title: "Track Order"},
516
- {id: "support", title: "Contact Support"},
517
- {id: "invoice", title: "View Invoice"}
518
- ])
519
- end
520
-
521
- def send_media_gallery(phone_number, images)
522
- images.each_with_index do |image_url, index|
523
- @client.send_image(phone_number, image_url, "Image #{index + 1}")
524
- end
525
- end
526
-
527
- def send_welcome_message(phone_number, user_name)
528
- # Upload and send personalized image
529
- File.open("welcome_images/#{user_name.downcase}.jpg", "rb") do |file|
530
- media_id = @client.upload_media(file, "image/jpeg", "welcome.jpg")
531
- @client.send_image(phone_number, media_id, "Welcome #{user_name}! 🎉")
532
- end
533
- end
52
+ config = FlowChat::Whatsapp::Configuration.new(:support).tap do |c|
53
+ c.access_token = tenant.whatsapp_access_token
54
+ c.phone_number_id = tenant.whatsapp_phone_number_id
55
+ c.verify_token = tenant.whatsapp_verify_token
56
+ c.app_secret = tenant.whatsapp_app_secret
534
57
  end
535
58
 
536
- # Usage in controllers or background jobs
537
- service = WhatsappNotificationService.new(:main_account)
538
- service.send_order_confirmation("+1234567890", "ORD-123")
539
- ```
540
-
541
- ## 🏗️ Template Manager
542
-
543
- The `FlowChat::Whatsapp::TemplateManager` provides utilities for managing WhatsApp message templates, which are required for initiating conversations with users.
544
-
545
- ### Template Manager Setup
546
-
547
- ```ruby
548
- # Using default configuration
549
- template_manager = FlowChat::Whatsapp::TemplateManager.new
550
-
551
- # Using specific configuration
552
- config = FlowChat::Whatsapp::Configuration.get(:my_account)
553
- template_manager = FlowChat::Whatsapp::TemplateManager.new(config)
554
- ```
555
-
556
- ### Sending Template Messages
557
-
558
- ```ruby
559
- # Basic template message
560
- template_manager.send_template(
561
- to: "+1234567890",
562
- template_name: "hello_world",
563
- language: "en_US",
564
- components: []
565
- )
566
-
567
- # Template with parameters
568
- components = [
569
- {
570
- type: "body",
571
- parameters: [
572
- {type: "text", text: "John Doe"},
573
- {type: "text", text: "Order #12345"}
574
- ]
575
- }
576
- ]
577
-
578
- template_manager.send_template(
579
- to: "+1234567890",
580
- template_name: "order_update",
581
- language: "en_US",
582
- components: components
583
- )
584
- ```
585
-
586
- ### Pre-built Template Methods
587
-
588
- ```ruby
589
- # Send welcome template (uses Meta's default hello_world template)
590
- template_manager.send_welcome_template(
591
- to: "+1234567890",
592
- name: "John Doe" # Optional personalization
593
- )
594
-
595
- # Send notification template
596
- template_manager.send_notification_template(
597
- to: "+1234567890",
598
- message: "Your order has shipped! Track it with the link below.",
599
- button_text: "Track Package" # Optional quick reply button
600
- )
601
- ```
602
-
603
- ### Template Management
604
-
605
- ```ruby
606
- # Create a new template (requires Meta approval)
607
- template_manager.create_template(
608
- name: "order_confirmation",
609
- category: "UTILITY", # AUTHENTICATION, MARKETING, UTILITY
610
- language: "en_US",
611
- components: [
612
- {
613
- type: "HEADER",
614
- format: "TEXT",
615
- text: "Order Confirmed"
616
- },
617
- {
618
- type: "BODY",
619
- text: "Hi {{1}}, your order {{2}} has been confirmed and will arrive by {{3}}."
620
- },
621
- {
622
- type: "FOOTER",
623
- text: "Thank you for shopping with us!"
624
- },
625
- {
626
- type: "BUTTONS",
627
- buttons: [
628
- {
629
- type: "QUICK_REPLY",
630
- text: "Track Order"
631
- },
632
- {
633
- type: "QUICK_REPLY",
634
- text: "Cancel Order"
635
- }
636
- ]
637
- }
638
- ]
639
- )
640
-
641
- # List all templates
642
- templates = template_manager.list_templates
643
- templates["data"].each do |template|
644
- puts "Template: #{template["name"]} - Status: #{template["status"]}"
59
+ processor = FlowChat::Processor.new(self) do |cfg|
60
+ cfg.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
61
+ cfg.use_session_store FlowChat::Session::CacheSessionStore
645
62
  end
646
-
647
- # Check template status
648
- status = template_manager.template_status("template_id_here")
649
- puts "Template status: #{status["status"]}" # PENDING, APPROVED, REJECTED
650
63
  ```
651
64
 
652
- ### Template Categories
653
-
654
- - **AUTHENTICATION**: One-time passwords, account verification
655
- - **MARKETING**: Promotional messages, newsletters (requires opt-in)
656
- - **UTILITY**: Order updates, appointment reminders, account notifications
65
+ Passing a name to `new` registers the configuration under that name, so you can retrieve it later with `FlowChat::Whatsapp::Configuration.get(:support)`. For an unnamed configuration, use `FlowChat::Whatsapp::Configuration.new(nil)`. The configuration attributes are `access_token`, `phone_number_id`, `verify_token`, `app_secret`, `business_account_id`, and `skip_signature_validation` (set it to `true` to bypass the `X-Hub-Signature-256` check, for local testing only).
657
66
 
658
- ### Template Components
67
+ ## The flow is the same
659
68
 
660
69
  ```ruby
661
- # Complete template structure example
662
- components = [
663
- # Header (optional)
664
- {
665
- type: "HEADER",
666
- format: "TEXT", # or IMAGE, VIDEO, DOCUMENT
667
- text: "Your Receipt"
668
- },
669
-
670
- # Body (required)
671
- {
672
- type: "BODY",
673
- text: "Hi {{1}}, here's your receipt for order {{2}} totaling {{3}}."
674
- },
675
-
676
- # Footer (optional)
677
- {
678
- type: "FOOTER",
679
- text: "Questions? Reply to this message."
680
- },
681
-
682
- # Buttons (optional)
683
- {
684
- type: "BUTTONS",
685
- buttons: [
686
- {
687
- type: "QUICK_REPLY",
688
- text: "Download PDF"
689
- },
690
- {
691
- type: "URL",
692
- text: "View Online",
693
- url: "https://example.com/receipt/{{1}}"
694
- },
695
- {
696
- type: "PHONE_NUMBER",
697
- text: "Call Support",
698
- phone_number: "+1234567890"
699
- }
700
- ]
701
- }
702
- ]
703
- ```
704
-
705
- ### Integration with Services
706
-
707
- ```ruby
708
- class OrderNotificationService
709
- def initialize
710
- @template_manager = FlowChat::Whatsapp::TemplateManager.new
711
- end
712
-
713
- def notify_order_confirmed(order)
714
- @template_manager.send_template(
715
- to: order.customer_phone,
716
- template_name: "order_confirmation",
717
- language: order.customer_locale || "en_US",
718
- components: [
719
- {
720
- type: "body",
721
- parameters: [
722
- {type: "text", text: order.customer_name},
723
- {type: "text", text: order.id},
724
- {type: "text", text: order.estimated_delivery.strftime("%B %d")}
725
- ]
726
- }
727
- ]
728
- )
729
- end
730
-
731
- def notify_order_shipped(order)
732
- @template_manager.send_notification_template(
733
- to: order.customer_phone,
734
- message: "Great news! Order #{order.id} has shipped and is on its way.",
735
- button_text: "Track Package"
736
- )
737
- end
738
- end
739
- ```
740
-
741
- ## 📁 Media Upload & Download
742
-
743
- ### Uploading Files
744
-
745
- ```ruby
746
- # Upload local file
747
- client = FlowChat::Whatsapp::Client.new(config)
748
- media_id = client.upload_media("/path/to/file.pdf", "application/pdf", "document.pdf")
749
-
750
- # Upload from IO
751
- File.open("/path/to/image.jpg", "rb") do |file|
752
- media_id = client.upload_media(file, "image/jpeg", "photo.jpg")
753
- end
754
-
755
- # Use uploaded media ID
756
- client.send_document("+1234567890", media_id, "Here's your document")
757
- ```
758
-
759
- ### Downloading Media
760
-
761
- ```ruby
762
- # Get media URL from ID
763
- media_url = client.get_media_url(media_id)
764
-
765
- # Download media content
766
- media_content = client.download_media(media_id)
767
-
768
- # Save to file
769
- File.write("/tmp/downloaded_media", media_content, mode: "wb")
770
- ```
771
-
772
- ## ⚙️ Processing Modes
773
-
774
- FlowChat supports three distinct message processing modes to handle different deployment scenarios and performance requirements. Configure the mode in your initializer:
775
-
776
- ```ruby
777
- # config/initializers/flowchat.rb
778
- FlowChat::Config.whatsapp.message_handling_mode = :inline # Default
779
- ```
780
-
781
- ### Mode Comparison
782
-
783
- | Feature | Inline | Background | Simulator |
784
- |---------|--------|------------|-----------|
785
- | **Response Speed** | Immediate | Async (queued) | Immediate |
786
- | **Webhook Timeout Risk** | High | None | None |
787
- | **Scalability** | Limited | High | N/A (testing only) |
788
- | **Setup Complexity** | Simple | Moderate | Simple |
789
- | **Production Ready** | Yes | Yes | No (development only) |
790
- | **Debugging** | Easy | Moderate | Easy |
791
- | **Message Delivery** | Real WhatsApp | Real WhatsApp | Simulated |
792
-
793
- ### Inline Mode (Default)
794
-
795
- Messages are processed synchronously:
796
-
797
- ```ruby
798
- FlowChat::Config.whatsapp.message_handling_mode = :inline
799
- ```
800
-
801
- **Pros**: Simple, immediate responses
802
- **Cons**: Webhook timeouts for slow operations
803
-
804
- ### Simulator Mode
805
-
806
- Returns response data instead of sending via WhatsApp API, perfect for development and testing:
807
-
808
- ```ruby
809
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
810
- ```
811
-
812
- #### Simulator Features
813
-
814
- - **No API calls**: Messages are not sent to WhatsApp
815
- - **Response inspection**: Returns full message payload in HTTP response
816
- - **Flow debugging**: Test conversation flows without real phone numbers
817
- - **No webhook validation**: Bypasses signature validation for easier testing
818
- - **Fast iteration**: Immediate feedback without waiting for WhatsApp delivery
819
-
820
- #### Simulator Response Format
821
-
822
- Instead of sending to WhatsApp, the simulator returns JSON with the message payload:
823
-
824
- ```json
825
- {
826
- "mode": "simulator",
827
- "message_sent": true,
828
- "response_data": {
829
- "messaging_product": "whatsapp",
830
- "to": "+1234567890",
831
- "type": "interactive",
832
- "interactive": {
833
- "type": "button",
834
- "body": {"text": "What would you like to do?"},
835
- "action": {
836
- "buttons": [
837
- {"type": "reply", "reply": {"id": "option1", "title": "View Orders"}},
838
- {"type": "reply", "reply": {"id": "option2", "title": "Track Package"}}
839
- ]
840
- }
841
- }
842
- },
843
- "config_used": "default",
844
- "timestamp": "2024-01-15T10:30:00Z"
845
- }
846
- ```
847
-
848
- #### Using Simulator in Development
849
-
850
- ```ruby
851
- # Enable simulator conditionally
852
- processor = FlowChat::Processor.new(self) do |config|
853
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
854
- config.use_session_store FlowChat::Session::CacheSessionStore
855
- end
856
-
857
- # Set mode globally in development
858
- FlowChat::Config.whatsapp.message_handling_mode = Rails.env.development? ? :simulator : :inline
859
-
860
- # Or enable per-request (useful for testing)
861
- # Add ?simulator=true to webhook URL for one-off testing
862
- ```
863
-
864
- #### Testing with Simulator
865
-
866
- ```ruby
867
- # test/integration/whatsapp_simulator_test.rb
868
- class WhatsappSimulatorTest < ActionDispatch::IntegrationTest
869
- def setup
870
- # Force simulator mode for tests
871
- @original_mode = FlowChat::Config.whatsapp.message_handling_mode
872
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
873
- end
874
-
875
- def teardown
876
- FlowChat::Config.whatsapp.message_handling_mode = @original_mode
877
- end
878
-
879
- def test_welcome_flow_buttons
880
- # Simulate WhatsApp webhook
881
- webhook_data = {
882
- entry: [{
883
- changes: [{
884
- value: {
885
- messages: [{
886
- from: "1234567890",
887
- id: "msg_123",
888
- type: "text",
889
- text: { body: "hello" }
890
- }],
891
- contacts: [{
892
- profile: { name: "Test User" }
893
- }]
894
- }
895
- }]
896
- }]
897
- }
898
-
899
- post "/whatsapp/webhook", params: webhook_data
900
-
901
- assert_response :success
902
- response_json = JSON.parse(response.body)
903
-
904
- assert_equal "simulator", response_json["mode"]
905
- assert_equal "interactive", response_json["response_data"]["type"]
906
- assert response_json["response_data"]["interactive"]["action"]["buttons"].present?
907
- end
908
- end
909
- ```
910
-
911
- **Pros**: Perfect for testing, fast development, no API costs, detailed response inspection
912
- **Cons**: Messages aren't actually sent, doesn't test real WhatsApp behavior
913
-
914
- ## 🔒 Security & Validation
915
-
916
- ### Webhook Signature Validation
917
-
918
- FlowChat automatically validates WhatsApp webhook signatures using HMAC-SHA256 to ensure requests are genuinely from WhatsApp and haven't been tampered with.
919
-
920
- #### How Signature Validation Works
921
-
922
- ```ruby
923
- # 1. Extract signature from X-Hub-Signature-256 header
924
- # 2. Calculate HMAC-SHA256 hash of request body using app_secret
925
- # 3. Compare calculated signature with provided signature using secure comparison
926
- # 4. Reject request if signatures don't match
927
- ```
928
-
929
- #### Production Configuration (Required)
930
-
931
- ```ruby
932
- # config/credentials/production.yml
933
- whatsapp:
934
- app_secret: "your_whatsapp_app_secret_from_meta"
935
- # ... other settings
936
-
937
- # The gateway automatically validates signatures
938
- processor = FlowChat::Processor.new(self) do |config|
939
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
940
- # Signature validation is enabled by default
941
- end
942
- ```
943
-
944
- #### Development Configuration Options
945
-
946
- ```ruby
947
- # Option 1: Use real app_secret (recommended for staging)
948
- config = FlowChat::Whatsapp::Configuration.new
949
- config.app_secret = "your_whatsapp_app_secret"
950
- config.skip_signature_validation = false # Default behavior
951
-
952
- # Option 2: Explicitly disable validation (development only)
953
- config = FlowChat::Whatsapp::Configuration.new
954
- config.app_secret = nil # Not required when disabled
955
- config.skip_signature_validation = true # Must be explicitly set to true
956
-
957
- # Option 3: Environment-based configuration
958
- config = FlowChat::Whatsapp::Configuration.new
959
- config.app_secret = Rails.env.production? ? "real_secret" : nil
960
- config.skip_signature_validation = !Rails.env.production?
961
- ```
962
-
963
- #### Configuration Error Handling
964
-
965
- The gateway will raise `FlowChat::Whatsapp::ConfigurationError` if:
966
-
967
- ```ruby
968
- # Missing app_secret with validation enabled (default)
969
- # Error: "WhatsApp app_secret is required for webhook signature validation"
970
-
971
- # Invalid webhook signature received
972
- # Returns HTTP 401 Unauthorized
973
-
974
- # Valid configuration examples:
975
- config.app_secret = "secret"; config.skip_signature_validation = false # ✅ Secure
976
- config.app_secret = nil; config.skip_signature_validation = true # ✅ Explicitly disabled
977
- config.app_secret = "secret"; config.skip_signature_validation = true # ✅ Secret provided but validation disabled
978
-
979
- # Invalid configuration:
980
- config.app_secret = nil; config.skip_signature_validation = false # ❌ Error
981
- ```
982
-
983
- #### Security Implementation Details
984
-
985
- ```ruby
986
- # The gateway uses secure comparison to prevent timing attacks
987
- def valid_webhook_signature?(request)
988
- # Extract signature from X-Hub-Signature-256 header
989
- signature_header = request.headers["X-Hub-Signature-256"]
990
- expected_signature = signature_header.sub("sha256=", "")
991
-
992
- # Calculate HMAC-SHA256 of request body
993
- body = request.body.read
994
- calculated_signature = OpenSSL::HMAC.hexdigest(
995
- OpenSSL::Digest.new("SHA256"),
996
- @config.app_secret,
997
- body
998
- )
999
-
1000
- # Use secure comparison to prevent timing attacks
1001
- ActiveSupport::SecurityUtils.secure_compare(expected_signature, calculated_signature)
1002
- end
1003
- ```
1004
-
1005
- ⚠️ **Security Warning**: Never disable signature validation in production environments.
1006
-
1007
- ### Error Handling
1008
-
1009
- ```ruby
1010
- def webhook
1011
- processor = FlowChat::Processor.new(self) do |config|
1012
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
1013
- config.use_session_store FlowChat::Session::CacheSessionStore
1014
- end
1015
-
1016
- processor.run WelcomeFlow, :main_page
1017
- rescue FlowChat::Whatsapp::ConfigurationError => e
1018
- Rails.logger.error "WhatsApp configuration error: #{e.message}"
1019
- head :internal_server_error
1020
- rescue => e
1021
- Rails.logger.error "WhatsApp webhook error: #{e.message}"
1022
- head :internal_server_error
1023
- end
1024
- ```
1025
-
1026
- ### Configuration Validation
1027
-
1028
- FlowChat will raise a `ConfigurationError` if:
1029
- - `app_secret` is missing and validation is not explicitly disabled
1030
- - Required credentials are missing
1031
- - Invalid webhook signatures are received
1032
-
1033
- ## 🏢 Multi-Tenant Support
1034
-
1035
- ### Named Configurations
1036
-
1037
- Register configurations by name for different accounts:
1038
-
1039
- ```ruby
1040
- # config/initializers/whatsapp_configs.rb
1041
- tenant_a_config = FlowChat::Whatsapp::Configuration.new(:tenant_a)
1042
- tenant_a_config.access_token = "tenant_a_token"
1043
- tenant_a_config.phone_number_id = "tenant_a_phone"
1044
- tenant_a_config.verify_token = "tenant_a_verify"
1045
- tenant_a_config.app_secret = "tenant_a_secret"
1046
-
1047
- tenant_b_config = FlowChat::Whatsapp::Configuration.new(:tenant_b)
1048
- tenant_b_config.access_token = "tenant_b_token"
1049
- tenant_b_config.phone_number_id = "tenant_b_phone"
1050
- tenant_b_config.verify_token = "tenant_b_verify"
1051
- tenant_b_config.app_secret = "tenant_b_secret"
1052
- ```
1053
-
1054
- ### Configuration API
1055
-
1056
- The configuration system provides several methods for managing named configurations:
1057
-
1058
- ```ruby
1059
- # Check if a configuration exists
1060
- FlowChat::Whatsapp::Configuration.exists?(:tenant_a) # => true/false
1061
-
1062
- # Get configuration by name (raises error if not found)
1063
- config = FlowChat::Whatsapp::Configuration.get(:tenant_a)
1064
-
1065
- # Get configuration with fallback
1066
- config = FlowChat::Whatsapp::Configuration.get(:tenant_a) rescue FlowChat::Whatsapp::Configuration.from_credentials
1067
-
1068
- # Register configuration programmatically
1069
- config = FlowChat::Whatsapp::Configuration.new(:dynamic_tenant)
1070
- config.access_token = "token_here"
1071
- config.phone_number_id = "phone_id_here"
1072
- # ... other settings
1073
- ```
1074
-
1075
- ### Using Named Configurations
70
+ class RegistrationFlow < FlowChat::Flow
71
+ def main_page
72
+ name = app.screen(:name) { |prompt| prompt.ask "What's your name?" }
1076
73
 
1077
- ```ruby
1078
- class MultiTenantWhatsappController < ApplicationController
1079
- def webhook
1080
- tenant = determine_tenant(request)
1081
-
1082
- # Verify configuration exists
1083
- unless FlowChat::Whatsapp::Configuration.exists?(tenant)
1084
- Rails.logger.error "No WhatsApp configuration found for tenant: #{tenant}"
1085
- head :not_found
1086
- return
1087
- end
1088
-
1089
- whatsapp_config = FlowChat::Whatsapp::Configuration.get(tenant)
1090
-
1091
- processor = FlowChat::Processor.new(self) do |config|
1092
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
1093
- config.use_session_store FlowChat::Session::CacheSessionStore
1094
- # Use tenant-specific session boundaries
1095
- config.use_session_config(
1096
- boundaries: [:flow, :platform, :tenant],
1097
- identifier: :msisdn,
1098
- tenant: tenant
1099
- )
74
+ email = app.screen(:email) do |prompt|
75
+ prompt.ask "Your email?", validate: ->(input) { "Invalid email" unless input.include?("@") }
1100
76
  end
1101
77
 
1102
- processor.run WelcomeFlow, :main_page
1103
- end
1104
-
1105
- private
1106
-
1107
- def determine_tenant(request)
1108
- # Multiple strategies for tenant detection
1109
- if request.subdomain.present?
1110
- request.subdomain.to_sym
1111
- elsif request.path.start_with?('/api/')
1112
- # Extract from API path: /api/v1/tenants/acme/whatsapp
1113
- request.path.split('/')[4]&.to_sym
1114
- elsif request.headers['X-Tenant-ID'].present?
1115
- request.headers['X-Tenant-ID'].to_sym
1116
- else
1117
- :default
1118
- end
78
+ app.say "Welcome #{name}!"
1119
79
  end
1120
80
  end
1121
81
  ```
1122
82
 
1123
- ### Background Jobs with Multi-Tenancy
83
+ ## How choices render
1124
84
 
1125
- ```ruby
1126
- class WhatsappMessageJob < ApplicationJob
1127
- include FlowChat::Whatsapp::SendJobSupport
85
+ FlowChat picks the WhatsApp interactive type from the number of choices:
1128
86
 
1129
- # SendJobSupport automatically handles configuration resolution
1130
- # It will use send_data[:config_name] if provided, otherwise fallback to default
87
+ - 3 choices or fewer render as reply buttons.
88
+ - 4 to 10 render as a list.
89
+ - Above 10 there is no interactive surface left: the options go straight into the message body, one per line, numbered.
1131
90
 
1132
- def on_whatsapp_send_success(send_data, result)
1133
- # Log with tenant context
1134
- tenant = send_data[:config_name] || :default
1135
- Rails.logger.info "Message sent for tenant #{tenant}: #{result["messages"]&.first&.dig("id")}"
1136
- end
1137
- end
1138
- ```
91
+ Button and list row titles are only numbered when they need to be. FlowChat truncates each title to fit (20 characters for a button, 24 for a list row) and checks the whole set: if any title had to be truncated, or if two choices land on the same title, the titles as displayed can no longer identify a choice on their own. When that happens, every title in the set gets prefixed with its 1-based position ("1. ", "2. ", and so on), not just the ones that collided, so a stray "2." never appears next to a title with no "1." beside it. A short menu of distinct options (`Yes` / `No`) stays unprefixed; a menu with a long label, or with two choices sharing a label (two accounts both named "Savings"), gets every title numbered (`1. Yes` / `2. No`, `1. Transfer to savin...` / `2. Transfer to salar...`).
1139
92
 
1140
- ### Dynamic Configuration Management
93
+ A user can reply by tapping, by typing the title exactly as shown, or - only when the screen was numbered - by typing the number. All of these resolve to the same choice key, so your flow reads `select` results the same way regardless of which one the user did. Above 10 choices, where nothing is tappable, the options go straight into the message body, one per line, always numbered, and a typed number is the only way to reply.
1141
94
 
1142
- ```ruby
1143
- class TenantConfigurationService
1144
- def self.setup_tenant(tenant_name, credentials)
1145
- config = FlowChat::Whatsapp::Configuration.new(tenant_name)
1146
- config.access_token = credentials[:access_token]
1147
- config.phone_number_id = credentials[:phone_number_id]
1148
- config.verify_token = credentials[:verify_token]
1149
- config.app_secret = credentials[:app_secret]
1150
- config.business_account_id = credentials[:business_account_id]
1151
-
1152
- # Validate configuration before registering
1153
- unless config.valid?
1154
- raise "Invalid WhatsApp configuration for tenant: #{tenant_name}"
1155
- end
1156
-
1157
- Rails.logger.info "WhatsApp configuration registered for tenant: #{tenant_name}"
1158
- end
95
+ ## Media
1159
96
 
1160
- def self.remove_tenant(tenant_name)
1161
- # Note: The current implementation doesn't provide a removal method
1162
- # This would need to be added to the Configuration class
1163
- Rails.logger.info "TODO: Remove configuration for tenant: #{tenant_name}"
1164
- end
1165
- end
1166
-
1167
- # Usage in tenant onboarding
1168
- TenantConfigurationService.setup_tenant(:new_client, {
1169
- access_token: "EAAxxxxxxxxx",
1170
- phone_number_id: "1234567890",
1171
- verify_token: "my_verify_token",
1172
- app_secret: "app_secret_here",
1173
- business_account_id: "business_account_id"
1174
- })
1175
- ```
1176
-
1177
- ## 🧪 Testing & Development
1178
-
1179
- ### Simulator Mode
1180
-
1181
- Enable the simulator for testing during development:
97
+ Read inbound media through `app.media`, an Array of `FlowChat::Media`:
1182
98
 
1183
99
  ```ruby
1184
- # Enable simulator in development
1185
- processor = FlowChat::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
1186
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
1187
- config.use_session_store FlowChat::Session::CacheSessionStore
100
+ photo = app.media.first
101
+ if photo
102
+ photo.type # => :image
103
+ photo.mime_type # => "image/jpeg"
104
+ bytes = photo.download
1188
105
  end
1189
106
  ```
1190
107
 
1191
- The simulator provides:
1192
- - Web interface for testing flows
1193
- - No actual WhatsApp API calls
1194
- - Response data returned in HTTP response
1195
- - Secure cookie-based authentication
1196
-
1197
- ### Testing Flows
108
+ Send media outbound by passing `media:` to `ask` or `say`:
1198
109
 
1199
110
  ```ruby
1200
- # test/flow_chat/welcome_flow_test.rb
1201
- require 'test_helper'
1202
-
1203
- class WelcomeFlowTest < ActiveSupport::TestCase
1204
- include FlowChat::TestHelpers
1205
-
1206
- def test_main_page_flow
1207
- # Simulate WhatsApp input
1208
- session = create_test_session
1209
- app = create_test_app(session: session)
1210
- flow = WelcomeFlow.new(app)
1211
-
1212
- # Test the flow
1213
- simulate_input(app, "John")
1214
- simulate_input(app, "info")
1215
-
1216
- response = flow.main_page
1217
-
1218
- assert_includes response.last, "Located at 123 Main Street"
1219
- end
1220
- end
111
+ app.say "Your receipt", media: { type: :document, url: "https://example.com/receipt.pdf" }
1221
112
  ```
1222
113
 
1223
- ## 📊 Instrumentation & Monitoring
114
+ The WhatsApp client also exposes direct senders (`send_image`, `send_document`, `send_audio`, `send_video`, `send_sticker`, `send_template`) and `upload_media`, which uploads a file and returns a media id you can reuse.
1224
115
 
1225
- FlowChat includes built-in instrumentation for WhatsApp events:
116
+ Media and choices combine, but media never changes which choice surface renders. 3 choices or fewer is the one case WhatsApp can carry both in a single message: the media becomes the header on the reply buttons.
1226
117
 
1227
118
  ```ruby
1228
- # Events automatically tracked:
1229
- # - FlowChat::Events::MESSAGE_RECEIVED
1230
- # - FlowChat::Events::MESSAGE_SENT
1231
- # - FlowChat::Events::MEDIA_UPLOAD
1232
- # - FlowChat::Events::WEBHOOK_VERIFIED
1233
- # - FlowChat::Events::WEBHOOK_FAILED
1234
-
1235
- # Custom instrumentation
1236
- FlowChat.instrument("custom.whatsapp.event", {
1237
- user_id: app.user_id,
1238
- action: "custom_action",
1239
- metadata: { key: "value" }
1240
- })
1241
- ```
1242
-
1243
- ## 🚨 Troubleshooting
1244
-
1245
- ### Common Issues
1246
-
1247
- **1. Configuration Error: app_secret required**
1248
- ```
1249
- WhatsApp app_secret is required for webhook signature validation.
1250
- ```
1251
- **Solution**: Configure `WHATSAPP_APP_SECRET` or disable validation explicitly.
1252
-
1253
- **2. Invalid webhook signature**
1254
- ```
1255
- Invalid webhook signature received
1256
- ```
1257
- **Solution**: Verify your `app_secret` matches your WhatsApp app configuration.
1258
-
1259
- **3. Media upload fails**
1260
- ```
1261
- Media upload failed: Invalid mime type
1262
- ```
1263
- **Solution**: Ensure MIME type is correctly specified and supported.
1264
-
1265
- **4. Background job class not found**
1266
- ```
1267
- Background mode requested but no WhatsappMessageJob found
1268
- ```
1269
- **Solution**: Create the background job class or use inline mode.
1270
-
1271
- ### Debug Mode
1272
-
1273
- Enable debug logging:
1274
-
1275
- ```ruby
1276
- # config/environments/development.rb
1277
- config.log_level = :debug
1278
-
1279
- # This will log:
1280
- # - Webhook signature validation
1281
- # - Message parsing and extraction
1282
- # - API request/response details
1283
- # - Configuration loading
1284
- ```
1285
-
1286
- ### Environment-Specific Configuration
1287
-
1288
- ```ruby
1289
- # config/initializers/flowchat.rb
1290
- case Rails.env
1291
- when 'development'
1292
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
1293
- # Skip signature validation for easier testing
1294
-
1295
- when 'test'
1296
- FlowChat::Config.whatsapp.message_handling_mode = :simulator
1297
-
1298
- when 'staging'
1299
- FlowChat::Config.whatsapp.message_handling_mode = :inline
1300
- # Full security validation
1301
-
1302
- when 'production'
1303
- FlowChat::Config.whatsapp.message_handling_mode = :background
1304
- FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
1305
- # Maximum security and scalability
119
+ app.screen(:plan) do |prompt|
120
+ prompt.select "Choose a plan", { "basic" => "Basic", "pro" => "Pro" },
121
+ media: { type: :image, url: "https://example.com/plans.png" }
1306
122
  end
1307
123
  ```
1308
124
 
1309
- ## 🎯 Best Practices
1310
-
1311
- ### 1. Message Design
1312
- - Keep button titles under 20 characters
1313
- - Use clear, actionable text
1314
- - Include relevant emojis for visual appeal
1315
- - Test across different devices
1316
-
1317
- ### 2. Media Usage
1318
- - Use appropriate file sizes (< 16MB for documents)
1319
- - Provide descriptive captions
1320
- - Use supported formats (JPEG, PNG for images; PDF for documents)
1321
- - Consider bandwidth limitations
1322
-
1323
- ### 3. Flow Design
1324
- - Handle media uploads gracefully
1325
- - Provide clear error messages
1326
- - Use progressive disclosure for complex flows
1327
- - Test with real user scenarios
1328
-
1329
- ### 4. Security
1330
- - Always validate webhooks in production
1331
- - Use environment variables for secrets
1332
- - Log security events appropriately
1333
- - Monitor for unusual patterns
1334
-
1335
- ### 5. Performance
1336
- - Use background processing for heavy operations
1337
- - Cache frequently used data
1338
- - Monitor response times
1339
- - Handle timeouts gracefully
1340
-
1341
- ## 📚 API Reference
1342
-
1343
- ### Core Classes
1344
-
1345
- - `FlowChat::Whatsapp::Gateway::CloudApi` - Meta Cloud API gateway implementation
1346
- - `FlowChat::Whatsapp::Gateway::Twilio` - Twilio WhatsApp gateway implementation
1347
- - `FlowChat::Whatsapp::Client` - Meta Cloud API client for out-of-band messaging
1348
- - `FlowChat::Whatsapp::TwilioClient` - Twilio API client for out-of-band messaging
1349
- - `FlowChat::Whatsapp::Configuration` - Meta Cloud API configuration management
1350
- - `FlowChat::Whatsapp::TwilioConfiguration` - Twilio configuration management
1351
- - `FlowChat::Whatsapp::Renderer` - Message rendering logic
1352
-
1353
- ### Client Methods
125
+ From 4 to 10 choices there is no interactive surface left that can carry media: Meta's interactive message reference documents a `text`-only header for list messages, image and video and document headers are only defined for button messages. So the image goes out as its own message first, with no caption (the question is about to appear in the list body right behind it), followed by the list exactly as it would render with no media at all.
1354
126
 
1355
- | Method | Description | Parameters |
1356
- |--------|-------------|------------|
1357
- | `send_message(to, response)` | Send FlowChat response format | phone, response array |
1358
- | `send_text(to, text)` | Send text message | phone, message |
1359
- | `send_buttons(to, text, buttons)` | Send interactive buttons | phone, text, button array |
1360
- | `send_list(to, text, sections, button_text)` | Send interactive list | phone, text, sections, button text |
1361
- | `send_template(to, name, components, language)` | Send template message | phone, template name, components, language |
1362
- | `send_image(to, url_or_id, caption, mime_type)` | Send image | phone, url/media ID, caption, mime type |
1363
- | `send_document(to, url_or_id, caption, filename, mime_type)` | Send document | phone, url/media ID, caption, filename, mime type |
1364
- | `send_video(to, url_or_id, caption, mime_type)` | Send video | phone, url/media ID, caption, mime type |
1365
- | `send_audio(to, url_or_id, mime_type)` | Send audio | phone, url/media ID, mime type |
1366
- | `send_sticker(to, url_or_id, mime_type)` | Send sticker | phone, url/media ID, mime type |
1367
- | `upload_media(file_path_or_io, mime_type, filename)` | Upload file and get media ID | file path/IO, mime type, filename |
1368
- | `get_media_url(media_id)` | Get media URL from ID | media ID |
1369
- | `download_media(media_id)` | Download media content | media ID |
1370
- | `build_message_payload(response, to)` | Build WhatsApp API payload | FlowChat response, phone number |
127
+ Above 10 choices the options are already nothing but text, and a WhatsApp media message can carry a caption up to 1024 characters (documented for image, video, and document messages; audio and sticker messages have no caption field at all). When the media type supports a caption and the prompt plus the numbered options fit under that cap, FlowChat sends one message: the media with the whole numbered list as its caption. When either does not hold - a long option list, or an audio or sticker attachment - it falls back to the same two-message shape as the list rung: the media on its own, with no caption, followed by the numbered text.
1371
128
 
1372
- ### Template Manager Methods
129
+ ## Limits to keep in mind
1373
130
 
1374
- | Method | Description | Parameters |
1375
- |--------|-------------|------------|
1376
- | `send_template(to:, template_name:, language:, components:)` | Send template message | named parameters |
1377
- | `send_welcome_template(to:, name:)` | Send welcome template | phone, optional name |
1378
- | `send_notification_template(to:, message:, button_text:)` | Send notification template | phone, message, optional button |
1379
- | `create_template(name:, category:, language:, components:)` | Create new template | template details |
1380
- | `list_templates()` | List all templates | none |
1381
- | `template_status(template_id)` | Get template status | template ID |
131
+ | Area | Behavior on WhatsApp |
132
+ |---|---|
133
+ | Button titles | Reply-button titles are truncated to 20 characters. |
134
+ | List titles | List row titles are truncated to 24 characters; the full text is moved into the row description (up to 72 characters). |
135
+ | List size | A list holds at most 10 rows in a single section; above that there is no interactive surface left, and the options go into the numbered message body instead. |
136
+ | Media with choices | 3 or fewer: one message, buttons with a media header. 4 to 10: media sent separately, then the list. Above 10: one captioned media message when the caption fits under 1024 characters and the media type supports a caption (image, video, document), otherwise media sent separately, then the numbered text. Never more than 3 reply buttons, with or without media. |
137
+ | 24-hour window | WhatsApp only allows free-form messages within 24 hours of the user's last message. Outside that window you must send an approved template. FlowChat does not abstract this: `send_template` exists, but you manage templates and the window yourself. |
1382
138
 
1383
- ### Configuration Methods
139
+ ## Async
1384
140
 
1385
- | Method | Description | Parameters |
1386
- |--------|-------------|------------|
1387
- | `Configuration.from_credentials()` | Load from Rails credentials/ENV | none |
1388
- | `Configuration.new(name)` | Create named configuration | configuration name |
1389
- | `Configuration.register(name, config)` | Register configuration | name, config object |
1390
- | `Configuration.get(name)` | Get configuration by name | configuration name |
1391
- | `Configuration.exists?(name)` | Check if config exists | configuration name |
141
+ WhatsApp supports background processing. Acknowledge the webhook immediately and run the flow in a job with `use_async`. See [factory-pattern.md](../factory-pattern.md) and [async-background-processing.md](../async-background-processing.md).
1392
142
 
1393
- ---
143
+ ## Related
1394
144
 
1395
- This comprehensive guide covers everything you need to build sophisticated WhatsApp integrations with FlowChat. For more examples and advanced patterns, check the [examples directory](../../examples/) in the FlowChat repository.
145
+ - [Getting started](../getting-started.md)
146
+ - [Configuration](../configuration.md)
147
+ - [Instrumentation](../instrumentation.md)