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.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +43 -0
- data/CHANGELOG.md +26 -0
- data/README.md +166 -295
- data/Rakefile +12 -1
- data/SECURITY.md +1 -1
- data/docs/architecture.md +52 -479
- data/docs/async-background-processing.md +31 -265
- data/docs/configuration.md +106 -613
- data/docs/factory-pattern.md +27 -324
- data/docs/gateway-context-variables.md +140 -143
- data/docs/gateway-development.md +86 -650
- data/docs/getting-started.md +40 -379
- data/docs/instrumentation.md +88 -279
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +47 -951
- data/docs/platforms/ussd.md +38 -653
- data/docs/platforms/whatsapp.md +73 -1321
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +26 -297
- data/examples/http_controller.rb +12 -10
- data/examples/intercom_configuration_example.rb +19 -24
- data/examples/intercom_controller.rb +8 -20
- data/examples/multi_tenant_whatsapp_controller.rb +61 -166
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +86 -158
- data/examples/whatsapp_controller.rb +16 -15
- data/examples/whatsapp_media_examples.rb +27 -79
- data/lib/flow_chat/app.rb +129 -11
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +124 -1
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +24 -4
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation.rb +160 -1
- data/lib/flow_chat/intercom/client.rb +34 -28
- data/lib/flow_chat/intercom/configuration.rb +2 -49
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +57 -5
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +109 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +11 -2
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +47 -4
- data/lib/flow_chat/telegram/configuration.rb +2 -42
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
- data/lib/flow_chat/telegram/renderer.rb +10 -2
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +14 -7
- data/lib/flow_chat/whatsapp/configuration.rb +12 -51
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +254 -203
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
- data/lib/flow_chat/whatsapp/renderer.rb +121 -60
- data/lib/flow_chat.rb +0 -10
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +51 -3
- data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
data/docs/platforms/whatsapp.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
# WhatsApp
|
|
1
|
+
# WhatsApp
|
|
2
2
|
|
|
3
|
-
FlowChat
|
|
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
|
-
##
|
|
5
|
+
## Credentials
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
Equivalent environment variables: `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_BUSINESS_ACCOUNT_ID`.
|
|
18
19
|
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## Explicit and multi-tenant configuration
|
|
490
48
|
|
|
491
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
## The flow is the same
|
|
659
68
|
|
|
660
69
|
```ruby
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
## How choices render
|
|
1124
84
|
|
|
1125
|
-
|
|
1126
|
-
class WhatsappMessageJob < ApplicationJob
|
|
1127
|
-
include FlowChat::Whatsapp::SendJobSupport
|
|
85
|
+
FlowChat picks the WhatsApp interactive type from the number of choices:
|
|
1128
86
|
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
## Limits to keep in mind
|
|
1373
130
|
|
|
1374
|
-
|
|
|
1375
|
-
|
|
1376
|
-
|
|
|
1377
|
-
|
|
|
1378
|
-
|
|
|
1379
|
-
|
|
|
1380
|
-
| `
|
|
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
|
-
|
|
139
|
+
## Async
|
|
1384
140
|
|
|
1385
|
-
|
|
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
|
-
|
|
145
|
+
- [Getting started](../getting-started.md)
|
|
146
|
+
- [Configuration](../configuration.md)
|
|
147
|
+
- [Instrumentation](../instrumentation.md)
|