flow_chat 0.6.1 โ 0.7.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/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +84 -1229
- data/docs/configuration.md +337 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +306 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +9 -18
- data/examples/ussd_controller.rb +32 -38
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +48 -2
- data/lib/flow_chat/config.rb +5 -0
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +37 -4
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +6 -6
- data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +14 -0
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +8 -0
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +22 -11
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
data/README.md
CHANGED
@@ -1,43 +1,33 @@
|
|
1
1
|
# FlowChat
|
2
2
|
|
3
|
-
|
3
|
+
[](https://github.com/radioactive-labs/flow_chat/actions/workflows/ci.yml)
|
4
|
+
[](https://badge.fury.io/rb/flow_chat)
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
6
|
+
[](https://www.ruby-lang.org/)
|
7
|
+
[](https://rubyonrails.org/)
|
4
8
|
|
5
|
-
|
6
|
-
- ๐ฏ **Declarative Flow Definition** - Define conversation flows as Ruby classes
|
7
|
-
- ๐ **Automatic Session Management** - Persistent state across requests
|
8
|
-
- โ
**Input Validation & Transformation** - Built-in validation and data conversion
|
9
|
-
- ๐ **Middleware Architecture** - Flexible request processing pipeline
|
10
|
-
- ๐ฑ **USSD Gateway Support** - Currently supports Nalo gateways
|
11
|
-
- ๐ฌ **WhatsApp Integration** - Full WhatsApp Cloud API support with multiple processing modes and webhook signature validation
|
12
|
-
- ๐ง **Reusable WhatsApp Client** - Standalone client for out-of-band messaging
|
13
|
-
- ๐งช **Built-in Testing Tools** - Unified simulator for both USSD and WhatsApp testing
|
14
|
-
|
15
|
-
## Architecture Overview
|
16
|
-
|
17
|
-
FlowChat uses a **request-per-interaction** model where each user input creates a new request. The framework maintains conversation state through session storage while processing each interaction through a middleware pipeline.
|
9
|
+
FlowChat is a Rails framework for building sophisticated conversational workflows for USSD and WhatsApp messaging. Define multi-step conversations as Ruby classes with automatic session management, input validation, and cross-platform compatibility.
|
18
10
|
|
19
|
-
|
20
|
-
User Input โ Gateway โ Session โ Pagination โ Custom โ Executor โ Flow โ Response
|
21
|
-
โ
|
22
|
-
Session Storage
|
23
|
-
```
|
11
|
+
## Key Features
|
24
12
|
|
25
|
-
**
|
26
|
-
- **
|
27
|
-
- **
|
28
|
-
- **
|
29
|
-
- **
|
30
|
-
- **
|
13
|
+
- ๐ฏ **Declarative Flow Definition** - Define conversations as Ruby classes
|
14
|
+
- ๐ **Automatic Session Management** - Persistent state across requests
|
15
|
+
- โ
**Input Validation & Transformation** - Built-in validation and data conversion
|
16
|
+
- ๐ฑ **USSD & WhatsApp Support** - Single codebase, multiple platforms
|
17
|
+
- ๐ฌ **Rich WhatsApp Features** - Interactive buttons, lists, media support
|
18
|
+
- ๐ง **Standalone WhatsApp Client** - Send messages outside of flows
|
19
|
+
- ๐ **Built-in Instrumentation** - Monitoring and metrics out of the box
|
20
|
+
- ๐งช **Testing Tools** - Built-in simulator for development and testing
|
31
21
|
|
32
22
|
## Installation
|
33
23
|
|
34
|
-
Add
|
24
|
+
Add to your Gemfile:
|
35
25
|
|
36
26
|
```ruby
|
37
27
|
gem 'flow_chat'
|
38
28
|
```
|
39
29
|
|
40
|
-
Then
|
30
|
+
Then run:
|
41
31
|
|
42
32
|
```bash
|
43
33
|
bundle install
|
@@ -45,30 +35,28 @@ bundle install
|
|
45
35
|
|
46
36
|
## Quick Start
|
47
37
|
|
48
|
-
|
38
|
+
### USSD Example
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
### 1. Create Your First Flow
|
53
|
-
|
54
|
-
Create a flow class in `app/flow_chat/welcome_flow.rb`:
|
40
|
+
Create a flow in `app/flow_chat/welcome_flow.rb`:
|
55
41
|
|
56
42
|
```ruby
|
57
43
|
class WelcomeFlow < FlowChat::Flow
|
58
44
|
def main_page
|
59
45
|
name = app.screen(:name) do |prompt|
|
60
|
-
prompt.ask "
|
46
|
+
prompt.ask "What's your name?",
|
61
47
|
transform: ->(input) { input.strip.titleize }
|
62
48
|
end
|
63
49
|
|
64
|
-
app.
|
50
|
+
language = app.screen(:language) do |prompt|
|
51
|
+
prompt.select "Choose language:", ["English", "French", "Spanish"]
|
52
|
+
end
|
53
|
+
|
54
|
+
app.say "Hello #{name}! Language set to #{language}."
|
65
55
|
end
|
66
56
|
end
|
67
57
|
```
|
68
58
|
|
69
|
-
|
70
|
-
|
71
|
-
Create a controller to handle USSD requests:
|
59
|
+
Create a controller:
|
72
60
|
|
73
61
|
```ruby
|
74
62
|
class UssdController < ApplicationController
|
@@ -85,193 +73,27 @@ class UssdController < ApplicationController
|
|
85
73
|
end
|
86
74
|
```
|
87
75
|
|
88
|
-
|
89
|
-
|
90
|
-
Add the route to `config/routes.rb`:
|
76
|
+
Add route in `config/routes.rb`:
|
91
77
|
|
92
78
|
```ruby
|
93
|
-
Rails.application.routes.draw do
|
94
79
|
post 'ussd' => 'ussd#process_request'
|
95
|
-
end
|
96
80
|
```
|
97
81
|
|
98
|
-
|
82
|
+
### WhatsApp Example
|
99
83
|
|
100
|
-
|
101
|
-
|
102
|
-
### 1. Configure WhatsApp Credentials
|
103
|
-
|
104
|
-
FlowChat supports two ways to configure WhatsApp credentials:
|
105
|
-
|
106
|
-
**Option A: Using Rails Credentials**
|
107
|
-
|
108
|
-
Add your WhatsApp credentials to Rails credentials:
|
109
|
-
|
110
|
-
```bash
|
111
|
-
rails credentials:edit
|
112
|
-
```
|
84
|
+
Configure credentials in `config/credentials.yml.enc`:
|
113
85
|
|
114
86
|
```yaml
|
115
87
|
whatsapp:
|
116
88
|
access_token: "your_access_token"
|
117
89
|
phone_number_id: "your_phone_number_id"
|
118
90
|
verify_token: "your_verify_token"
|
119
|
-
app_id: "your_app_id"
|
120
91
|
app_secret: "your_app_secret"
|
121
|
-
business_account_id: "your_business_account_id"
|
122
|
-
skip_signature_validation: false # Set to true only for development/testing
|
123
92
|
```
|
124
93
|
|
125
|
-
|
126
|
-
|
127
|
-
Alternatively, you can use environment variables:
|
128
|
-
|
129
|
-
```bash
|
130
|
-
# Add to your .env file or environment
|
131
|
-
export WHATSAPP_ACCESS_TOKEN="your_access_token"
|
132
|
-
export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
|
133
|
-
export WHATSAPP_VERIFY_TOKEN="your_verify_token"
|
134
|
-
export WHATSAPP_APP_ID="your_app_id"
|
135
|
-
export WHATSAPP_APP_SECRET="your_app_secret"
|
136
|
-
export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
|
137
|
-
export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false" # Set to "true" only for development/testing
|
138
|
-
```
|
139
|
-
|
140
|
-
FlowChat will automatically use Rails credentials first, falling back to environment variables if credentials are not available.
|
141
|
-
|
142
|
-
**Option C: Per-Setup Configuration**
|
143
|
-
|
144
|
-
For multi-tenant applications or when you need different WhatsApp accounts per endpoint:
|
94
|
+
Create a controller:
|
145
95
|
|
146
96
|
```ruby
|
147
|
-
# Create custom configuration
|
148
|
-
custom_config = FlowChat::Whatsapp::Configuration.new
|
149
|
-
custom_config.access_token = "your_specific_access_token"
|
150
|
-
custom_config.phone_number_id = "your_specific_phone_number_id"
|
151
|
-
custom_config.verify_token = "your_specific_verify_token"
|
152
|
-
custom_config.app_id = "your_specific_app_id"
|
153
|
-
custom_config.app_secret = "your_specific_app_secret"
|
154
|
-
custom_config.business_account_id = "your_specific_business_account_id"
|
155
|
-
custom_config.skip_signature_validation = false # Security setting
|
156
|
-
|
157
|
-
# Use in processor
|
158
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
159
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_config
|
160
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
161
|
-
end
|
162
|
-
```
|
163
|
-
|
164
|
-
๐ก **Tip**: See [examples/multi_tenant_whatsapp_controller.rb](examples/multi_tenant_whatsapp_controller.rb) for comprehensive multi-tenant and per-setup configuration examples.
|
165
|
-
|
166
|
-
### 2. Security Configuration
|
167
|
-
|
168
|
-
FlowChat includes robust security features for WhatsApp webhook validation:
|
169
|
-
|
170
|
-
**Webhook Signature Validation** (Recommended for Production)
|
171
|
-
|
172
|
-
FlowChat automatically validates WhatsApp webhook signatures using your app secret:
|
173
|
-
|
174
|
-
```ruby
|
175
|
-
# config/initializers/flowchat.rb
|
176
|
-
|
177
|
-
# Global security configuration
|
178
|
-
FlowChat::Config.simulator_secret = "your_secure_random_secret_for_simulator"
|
179
|
-
|
180
|
-
# WhatsApp security is configured per-configuration
|
181
|
-
# The app_secret from your WhatsApp configuration is used for webhook validation
|
182
|
-
```
|
183
|
-
|
184
|
-
**Security Options:**
|
185
|
-
|
186
|
-
```ruby
|
187
|
-
# Option 1: Full security (recommended for production)
|
188
|
-
custom_config.app_secret = "your_whatsapp_app_secret" # Required for signature validation
|
189
|
-
custom_config.skip_signature_validation = false # Default: enforce validation
|
190
|
-
|
191
|
-
# Option 2: Disable validation (development/testing only)
|
192
|
-
custom_config.app_secret = nil # Not required when disabled
|
193
|
-
custom_config.skip_signature_validation = true # Explicitly disable validation
|
194
|
-
```
|
195
|
-
|
196
|
-
โ ๏ธ **Security Warning**: Only disable signature validation in development/testing environments. Production environments should always validate webhook signatures using your WhatsApp app secret.
|
197
|
-
|
198
|
-
**Simulator Authentication**
|
199
|
-
|
200
|
-
The simulator mode requires authentication:
|
201
|
-
|
202
|
-
```ruby
|
203
|
-
# config/initializers/flowchat.rb
|
204
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
205
|
-
|
206
|
-
# Or use a dedicated secret
|
207
|
-
FlowChat::Config.simulator_secret = "your_secure_random_secret_here"
|
208
|
-
```
|
209
|
-
|
210
|
-
The simulator uses HMAC-SHA256 signed cookies for authentication with 24-hour expiration.
|
211
|
-
|
212
|
-
๐ **For comprehensive security documentation, see [SECURITY.md](SECURITY.md)**
|
213
|
-
|
214
|
-
### 3. Choose Message Handling Mode
|
215
|
-
|
216
|
-
FlowChat offers three WhatsApp message handling modes. Configure them in an initializer:
|
217
|
-
|
218
|
-
**Create an initializer** `config/initializers/flowchat.rb`:
|
219
|
-
|
220
|
-
```ruby
|
221
|
-
# config/initializers/flowchat.rb
|
222
|
-
|
223
|
-
# Configure WhatsApp message handling mode
|
224
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline # or :background, :simulator
|
225
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
226
|
-
```
|
227
|
-
|
228
|
-
**Inline Mode (Default)** - Process messages synchronously:
|
229
|
-
```ruby
|
230
|
-
# config/initializers/flowchat.rb
|
231
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
232
|
-
|
233
|
-
# app/controllers/whatsapp_controller.rb
|
234
|
-
class WhatsappController < ApplicationController
|
235
|
-
skip_forgery_protection
|
236
|
-
|
237
|
-
def webhook
|
238
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
239
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
240
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
241
|
-
end
|
242
|
-
|
243
|
-
processor.run WelcomeFlow, :main_page
|
244
|
-
end
|
245
|
-
end
|
246
|
-
```
|
247
|
-
|
248
|
-
**Background Mode** - Process flows synchronously, send responses asynchronously:
|
249
|
-
```ruby
|
250
|
-
# config/initializers/flowchat.rb
|
251
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
252
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
253
|
-
|
254
|
-
# app/controllers/whatsapp_controller.rb
|
255
|
-
class WhatsappController < ApplicationController
|
256
|
-
skip_forgery_protection
|
257
|
-
|
258
|
-
def webhook
|
259
|
-
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
260
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
261
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
262
|
-
end
|
263
|
-
|
264
|
-
processor.run WelcomeFlow, :main_page
|
265
|
-
end
|
266
|
-
end
|
267
|
-
```
|
268
|
-
|
269
|
-
**Simulator Mode** - Return response data instead of sending via WhatsApp API:
|
270
|
-
```ruby
|
271
|
-
# config/initializers/flowchat.rb
|
272
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
273
|
-
|
274
|
-
# app/controllers/whatsapp_controller.rb
|
275
97
|
class WhatsappController < ApplicationController
|
276
98
|
skip_forgery_protection
|
277
99
|
|
@@ -286,1068 +108,101 @@ class WhatsappController < ApplicationController
|
|
286
108
|
end
|
287
109
|
```
|
288
110
|
|
289
|
-
|
111
|
+
Add route:
|
290
112
|
|
291
113
|
```ruby
|
292
|
-
Rails.application.routes.draw do
|
293
114
|
match '/whatsapp/webhook', to: 'whatsapp#webhook', via: [:get, :post]
|
294
|
-
end
|
295
|
-
```
|
296
|
-
|
297
|
-
### 5. Enhanced Simulator Setup
|
298
|
-
|
299
|
-
FlowChat provides a powerful built-in simulator for testing flows in both USSD and WhatsApp modes. The simulator allows you to test different endpoints on your local server without needing actual USSD or WhatsApp infrastructure.
|
300
|
-
|
301
|
-
**Setup Simulator Controller:**
|
302
|
-
|
303
|
-
```ruby
|
304
|
-
# app/controllers/simulator_controller.rb
|
305
|
-
class SimulatorController < ApplicationController
|
306
|
-
include FlowChat::Simulator::Controller
|
307
|
-
|
308
|
-
def index
|
309
|
-
flowchat_simulator
|
310
|
-
end
|
311
|
-
|
312
|
-
protected
|
313
|
-
|
314
|
-
def configurations
|
315
|
-
{
|
316
|
-
ussd: {
|
317
|
-
name: "USSD Integration",
|
318
|
-
icon: "๐ฑ",
|
319
|
-
processor_type: "ussd",
|
320
|
-
provider: "nalo",
|
321
|
-
endpoint: "/ussd",
|
322
|
-
color: "#007bff"
|
323
|
-
},
|
324
|
-
whatsapp: {
|
325
|
-
name: "WhatsApp Integration",
|
326
|
-
icon: "๐ฌ",
|
327
|
-
processor_type: "whatsapp",
|
328
|
-
provider: "cloud_api",
|
329
|
-
endpoint: "/whatsapp/webhook",
|
330
|
-
color: "#25D366"
|
331
|
-
},
|
332
|
-
alternative_whatsapp: {
|
333
|
-
name: "Alternative WhatsApp Endpoint",
|
334
|
-
icon: "๐",
|
335
|
-
processor_type: "whatsapp",
|
336
|
-
provider: "cloud_api",
|
337
|
-
endpoint: "/alternative_whatsapp/webhook",
|
338
|
-
color: "#17a2b8"
|
339
|
-
}
|
340
|
-
}
|
341
|
-
end
|
342
|
-
|
343
|
-
def default_config_key
|
344
|
-
:local_whatsapp
|
345
|
-
end
|
346
|
-
|
347
|
-
def default_phone_number
|
348
|
-
"+1234567890"
|
349
|
-
end
|
350
|
-
|
351
|
-
def default_contact_name
|
352
|
-
"John Doe"
|
353
|
-
end
|
354
|
-
end
|
355
|
-
```
|
356
|
-
|
357
|
-
**Add Simulator Route:**
|
358
|
-
|
359
|
-
```ruby
|
360
|
-
# config/routes.rb
|
361
|
-
Rails.application.routes.draw do
|
362
|
-
get '/simulator' => 'simulator#index'
|
363
|
-
# ... other routes
|
364
|
-
end
|
365
|
-
```
|
366
|
-
|
367
|
-
**Configure Simulator Security:**
|
368
|
-
|
369
|
-
```ruby
|
370
|
-
# config/initializers/flowchat.rb
|
371
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
372
|
-
```
|
373
|
-
|
374
|
-
**Enable Simulator Mode in Controllers:**
|
375
|
-
|
376
|
-
For controllers that should support simulator mode, enable it in the processor:
|
377
|
-
|
378
|
-
```ruby
|
379
|
-
# app/controllers/whatsapp_controller.rb
|
380
|
-
class WhatsappController < ApplicationController
|
381
|
-
skip_forgery_protection
|
382
|
-
|
383
|
-
def webhook
|
384
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.local?) do |config|
|
385
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
386
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
387
|
-
end
|
388
|
-
|
389
|
-
processor.run WelcomeFlow, :main_page
|
390
|
-
end
|
391
|
-
end
|
392
|
-
```
|
393
|
-
|
394
|
-
This is enabled by default in `Rails.env.local?` (development and testing) environments.
|
395
|
-
|
396
|
-
**Simulator Features:**
|
397
|
-
- ๐ **Endpoint Toggle** - Switch between different integration endpoints
|
398
|
-
- ๐ฑ **USSD Mode** - Classic terminal simulation with pagination
|
399
|
-
- ๐ฌ **WhatsApp Mode** - Full WhatsApp interface with interactive elements
|
400
|
-
- ๐จ **Modern UI** - Beautiful, responsive interface
|
401
|
-
- ๐ **Request Logging** - View HTTP requests and responses in real-time
|
402
|
-
- ๐ง **Developer Tools** - Character counts, connection status, error handling
|
403
|
-
- ๐ **Secure Authentication** - HMAC-signed cookies with expiration
|
404
|
-
|
405
|
-
Visit `http://localhost:3000/simulator` to access the simulator interface and test your local endpoints.
|
406
|
-
|
407
|
-
## ๐ง Reusable WhatsApp Client
|
408
|
-
|
409
|
-
FlowChat provides a standalone WhatsApp client for out-of-band messaging:
|
410
|
-
|
411
|
-
```ruby
|
412
|
-
# Initialize client
|
413
|
-
config = FlowChat::Whatsapp::Configuration.from_credentials
|
414
|
-
client = FlowChat::Whatsapp::Client.new(config)
|
415
|
-
|
416
|
-
# Send text message
|
417
|
-
client.send_text("+1234567890", "Hello, World!")
|
418
|
-
|
419
|
-
# Send interactive buttons
|
420
|
-
client.send_buttons(
|
421
|
-
"+1234567890",
|
422
|
-
"Choose an option:",
|
423
|
-
[
|
424
|
-
{ id: 'option1', title: 'Option 1' },
|
425
|
-
{ id: 'option2', title: 'Option 2' }
|
426
|
-
]
|
427
|
-
)
|
428
|
-
|
429
|
-
# Send interactive list
|
430
|
-
client.send_list(
|
431
|
-
"+1234567890",
|
432
|
-
"Select from menu:",
|
433
|
-
[
|
434
|
-
{
|
435
|
-
title: "Services",
|
436
|
-
rows: [
|
437
|
-
{ id: 'service1', title: 'Service 1', description: 'Description 1' },
|
438
|
-
{ id: 'service2', title: 'Service 2', description: 'Description 2' }
|
439
|
-
]
|
440
|
-
}
|
441
|
-
]
|
442
|
-
)
|
443
|
-
|
444
|
-
# Handle media
|
445
|
-
media_url = client.get_media_url("media_id_123")
|
446
|
-
media_content = client.download_media("media_id_123")
|
447
|
-
```
|
448
|
-
|
449
|
-
### Out-of-Band Messaging Service Example
|
450
|
-
|
451
|
-
```ruby
|
452
|
-
class NotificationService
|
453
|
-
def initialize
|
454
|
-
@config = FlowChat::Whatsapp::Configuration.from_credentials
|
455
|
-
@client = FlowChat::Whatsapp::Client.new(@config)
|
456
|
-
end
|
457
|
-
|
458
|
-
def send_order_confirmation(phone_number, order_id, items, total)
|
459
|
-
item_list = items.map { |item| "โข #{item[:name]} x#{item[:quantity]}" }.join("\n")
|
460
|
-
|
461
|
-
@client.send_buttons(
|
462
|
-
phone_number,
|
463
|
-
"โ
Order Confirmed!\n\nOrder ##{order_id}\n\n#{item_list}\n\nTotal: $#{total}",
|
464
|
-
[
|
465
|
-
{ id: 'track_order', title: '๐ฆ Track Order' },
|
466
|
-
{ id: 'contact_support', title: '๐ฌ Contact Support' }
|
467
|
-
]
|
468
|
-
)
|
469
|
-
end
|
470
|
-
|
471
|
-
def send_appointment_reminder(phone_number, appointment)
|
472
|
-
@client.send_buttons(
|
473
|
-
phone_number,
|
474
|
-
"๐ฅ Appointment Reminder\n\n#{appointment[:service]} with #{appointment[:provider]}\n๐
#{appointment[:date]}\n๐ #{appointment[:time]}",
|
475
|
-
[
|
476
|
-
{ id: 'confirm', title: 'โ
Confirm' },
|
477
|
-
{ id: 'reschedule', title: '๐
Reschedule' },
|
478
|
-
{ id: 'cancel', title: 'โ Cancel' }
|
479
|
-
]
|
480
|
-
)
|
481
|
-
end
|
482
|
-
end
|
483
|
-
```
|
484
|
-
|
485
|
-
|
486
|
-
## Cross-Platform Compatibility
|
487
|
-
|
488
|
-
FlowChat provides a unified API that works across both USSD and WhatsApp platforms, with graceful degradation for platform-specific features. Under the hood, FlowChat uses a unified prompt architecture that ensures consistent behavior across platforms while optimizing the user experience for each channel.
|
489
|
-
|
490
|
-
### Shared Features (Both USSD & WhatsApp)
|
491
|
-
- โ
`app.screen()` - Interactive screens with prompts
|
492
|
-
- โ
`app.say()` - Send messages to users
|
493
|
-
- โ
`prompt.ask()` - Text input collection
|
494
|
-
- โ
`prompt.select()` - Menu selection (renders as numbered list in USSD, interactive buttons/lists in WhatsApp)
|
495
|
-
- โ
`prompt.yes?()` - Yes/no questions
|
496
|
-
- โ
`app.phone_number` - User's phone number
|
497
|
-
- โ
`app.message_id` - Unique message identifier
|
498
|
-
- โ
`app.timestamp` - Message timestamp
|
499
|
-
|
500
|
-
### WhatsApp-Only Features
|
501
|
-
- โ
`app.contact_name` - WhatsApp contact name (returns `nil` in USSD)
|
502
|
-
- โ
`app.location` - Location sharing data (returns `nil` in USSD)
|
503
|
-
- โ
`app.media` - Media file attachments (returns `nil` in USSD)
|
504
|
-
- โ
Rich interactive elements (buttons, lists) automatically generated from `prompt.select()`
|
505
|
-
|
506
|
-
This design allows you to write flows once and deploy them on both platforms, with WhatsApp users getting enhanced interactive features automatically.
|
507
|
-
|
508
|
-
## ๐ฑ Media Support
|
509
|
-
|
510
|
-
FlowChat supports rich media attachments for enhanced conversational experiences. Media can be attached to `ask()` and `say()` prompts, with automatic cross-platform optimization.
|
511
|
-
|
512
|
-
### Supported Media Types
|
513
|
-
|
514
|
-
- **๐ท Images** (`type: :image`) - Photos, screenshots, diagrams
|
515
|
-
- **๐ Documents** (`type: :document`) - PDFs, forms, receipts
|
516
|
-
- **๐ฅ Videos** (`type: :video`) - Tutorials, demos, explanations
|
517
|
-
- **๐ต Audio** (`type: :audio`) - Voice messages, recordings
|
518
|
-
- **๐ Stickers** (`type: :sticker`) - Fun visual elements
|
519
|
-
|
520
|
-
### Basic Usage
|
521
|
-
|
522
|
-
```ruby
|
523
|
-
class ProductFlow < FlowChat::Flow
|
524
|
-
def main_page
|
525
|
-
# โ
Text input with context image
|
526
|
-
feedback = app.screen(:feedback) do |prompt|
|
527
|
-
prompt.ask "What do you think of our new product?",
|
528
|
-
media: {
|
529
|
-
type: :image,
|
530
|
-
url: "https://cdn.example.com/products/new_product.jpg"
|
531
|
-
}
|
532
|
-
end
|
533
|
-
|
534
|
-
# โ
Send informational media
|
535
|
-
app.say "Thanks for your feedback! Here's what's coming next:",
|
536
|
-
media: {
|
537
|
-
type: :video,
|
538
|
-
url: "https://videos.example.com/roadmap.mp4"
|
539
|
-
}
|
540
|
-
|
541
|
-
# โ
Document with filename
|
542
|
-
app.say "Here's your receipt:",
|
543
|
-
media: {
|
544
|
-
type: :document,
|
545
|
-
url: "https://api.example.com/receipt.pdf",
|
546
|
-
filename: "receipt.pdf"
|
547
|
-
}
|
548
|
-
end
|
549
|
-
end
|
550
|
-
```
|
551
|
-
|
552
|
-
### Media Hash Format
|
553
|
-
|
554
|
-
```ruby
|
555
|
-
{
|
556
|
-
type: :image, # Required: :image, :document, :audio, :video, :sticker
|
557
|
-
url: "https://...", # Required: URL to the media file OR WhatsApp media ID
|
558
|
-
filename: "doc.pdf" # Optional: Only for documents
|
559
|
-
}
|
560
|
-
```
|
561
|
-
|
562
|
-
### Using WhatsApp Media IDs
|
563
|
-
|
564
|
-
For better performance and to avoid external dependencies, you can upload files to WhatsApp and use the media ID:
|
565
|
-
|
566
|
-
```ruby
|
567
|
-
# Upload a file first
|
568
|
-
client = FlowChat::Whatsapp::Client.new(config)
|
569
|
-
media_id = client.upload_media('path/to/image.jpg', 'image/jpeg')
|
570
|
-
|
571
|
-
# Then use the media ID in your flow
|
572
|
-
app.screen(:product_demo) do |prompt|
|
573
|
-
prompt.ask "What do you think?",
|
574
|
-
media: {
|
575
|
-
type: :image,
|
576
|
-
url: media_id # Use the media ID instead of URL
|
577
|
-
}
|
578
|
-
end
|
579
|
-
```
|
580
|
-
|
581
|
-
### Client Media Methods
|
582
|
-
|
583
|
-
The WhatsApp client provides methods for uploading and sending media:
|
584
|
-
|
585
|
-
```ruby
|
586
|
-
client = FlowChat::Whatsapp::Client.new(config)
|
587
|
-
|
588
|
-
# Upload media and get media ID
|
589
|
-
media_id = client.upload_media('image.jpg', 'image/jpeg')
|
590
|
-
media_id = client.upload_media(file_io, 'image/jpeg', 'photo.jpg')
|
591
|
-
|
592
|
-
# Send media directly
|
593
|
-
client.send_image("+1234567890", "https://example.com/image.jpg", "Caption")
|
594
|
-
client.send_image("+1234567890", media_id, "Caption")
|
595
|
-
|
596
|
-
# Send document with MIME type and filename
|
597
|
-
client.send_document("+1234567890", "https://example.com/doc.pdf", "Your receipt", "receipt.pdf", "application/pdf")
|
598
|
-
|
599
|
-
# Send other media types
|
600
|
-
client.send_video("+1234567890", "https://example.com/video.mp4", "Demo video", "video/mp4")
|
601
|
-
client.send_audio("+1234567890", "https://example.com/audio.mp3", "audio/mpeg")
|
602
|
-
client.send_sticker("+1234567890", "https://example.com/sticker.webp", "image/webp")
|
603
|
-
```
|
604
|
-
|
605
|
-
### Cross-Platform Behavior
|
606
|
-
|
607
|
-
**WhatsApp Experience:**
|
608
|
-
- Media is sent directly to the chat
|
609
|
-
- Prompt text becomes the media caption
|
610
|
-
- Rich, native messaging experience
|
611
|
-
|
612
|
-
**USSD Experience:**
|
613
|
-
- Media URL is included in text message
|
614
|
-
- Graceful degradation with clear media indicators
|
615
|
-
- Users can access media via the provided link
|
616
|
-
|
617
|
-
```ruby
|
618
|
-
# This code works on both platforms:
|
619
|
-
app.screen(:help) do |prompt|
|
620
|
-
prompt.ask "Describe your issue:",
|
621
|
-
media: {
|
622
|
-
type: :image,
|
623
|
-
url: "https://support.example.com/help_example.jpg"
|
624
|
-
}
|
625
|
-
end
|
626
|
-
```
|
627
|
-
|
628
|
-
**WhatsApp Result:** Image sent with caption "Describe your issue:"
|
629
|
-
|
630
|
-
**USSD Result:**
|
631
|
-
```
|
632
|
-
Describe your issue:
|
633
|
-
|
634
|
-
๐ท Image: https://support.example.com/help_example.jpg
|
635
115
|
```
|
636
116
|
|
637
117
|
## Core Concepts
|
638
118
|
|
639
119
|
### Flows and Screens
|
640
120
|
|
641
|
-
**Flows**
|
121
|
+
**Flows** define conversation logic. **Screens** collect user input with automatic validation:
|
642
122
|
|
643
123
|
```ruby
|
644
124
|
class RegistrationFlow < FlowChat::Flow
|
645
125
|
def main_page
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
end
|
651
|
-
|
652
|
-
age = app.screen(:age) do |prompt|
|
653
|
-
prompt.ask "Enter your age:",
|
654
|
-
convert: ->(input) { input.to_i },
|
655
|
-
validate: ->(input) { "Must be 18 or older" unless input >= 18 }
|
126
|
+
email = app.screen(:email) do |prompt|
|
127
|
+
prompt.ask "Enter email:",
|
128
|
+
validate: ->(input) { "Invalid email" unless input.include?("@") },
|
129
|
+
transform: ->(input) { input.downcase.strip }
|
656
130
|
end
|
657
131
|
|
658
|
-
|
659
|
-
|
660
|
-
app.say "Registration complete!"
|
661
|
-
end
|
662
|
-
|
663
|
-
private
|
664
|
-
|
665
|
-
def valid_phone?(phone)
|
666
|
-
phone.match?(/\A\+?[\d\s\-\(\)]+\z/)
|
667
|
-
end
|
668
|
-
|
669
|
-
def create_user(phone:, age:)
|
670
|
-
# Your user creation logic here
|
671
|
-
end
|
672
|
-
end
|
673
|
-
```
|
674
|
-
|
675
|
-
### Input Validation and Transformation
|
676
|
-
|
677
|
-
FlowChat provides powerful input processing capabilities:
|
678
|
-
|
679
|
-
```ruby
|
680
|
-
app.screen(:email) do |prompt|
|
681
|
-
prompt.ask "Enter your email:",
|
682
|
-
# Transform input before validation
|
683
|
-
transform: ->(input) { input.strip.downcase },
|
684
|
-
|
685
|
-
# Validate the input
|
686
|
-
validate: ->(input) {
|
687
|
-
"Invalid email format" unless input.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
688
|
-
},
|
689
|
-
|
690
|
-
# Convert to final format
|
691
|
-
convert: ->(input) { input }
|
692
|
-
end
|
693
|
-
```
|
694
|
-
|
695
|
-
### Menu Selection
|
696
|
-
|
697
|
-
Create selection menus with automatic validation:
|
698
|
-
|
699
|
-
```ruby
|
700
|
-
# Array-based choices
|
701
|
-
language = app.screen(:language) do |prompt|
|
702
|
-
prompt.select "Choose your language:", ["English", "French", "Spanish"]
|
703
|
-
end
|
704
|
-
|
705
|
-
# Hash-based choices (keys are returned values)
|
706
|
-
plan = app.screen(:plan) do |prompt|
|
707
|
-
prompt.select "Choose a plan:", {
|
708
|
-
"basic" => "Basic Plan ($10/month)",
|
709
|
-
"premium" => "Premium Plan ($25/month)",
|
710
|
-
"enterprise" => "Enterprise Plan ($100/month)"
|
711
|
-
}
|
712
|
-
end
|
713
|
-
```
|
714
|
-
|
715
|
-
### Yes/No Prompts
|
716
|
-
|
717
|
-
Simplified boolean input collection:
|
718
|
-
|
719
|
-
```ruby
|
720
|
-
confirmed = app.screen(:confirmation) do |prompt|
|
721
|
-
prompt.yes? "Do you want to proceed with the payment?"
|
132
|
+
confirmed = app.screen(:confirm) do |prompt|
|
133
|
+
prompt.yes? "Create account for #{email}?"
|
722
134
|
end
|
723
135
|
|
724
136
|
if confirmed
|
725
|
-
|
726
|
-
|
727
|
-
else
|
728
|
-
app.say "Payment cancelled."
|
729
|
-
end
|
730
|
-
```
|
731
|
-
|
732
|
-
## Advanced Features
|
733
|
-
|
734
|
-
### Session Management and Flow State
|
735
|
-
|
736
|
-
FlowChat automatically manages session state across requests. Each screen's result is cached, so users can navigate back and forth without losing data:
|
737
|
-
|
738
|
-
```ruby
|
739
|
-
class OrderFlow < FlowChat::Flow
|
740
|
-
def main_page
|
741
|
-
# These values persist across requests
|
742
|
-
product = app.screen(:product) { |p| p.select "Choose product:", products }
|
743
|
-
quantity = app.screen(:quantity) { |p| p.ask "Quantity:", convert: :to_i }
|
744
|
-
|
745
|
-
# Show summary
|
746
|
-
total = calculate_total(product, quantity)
|
747
|
-
confirmed = app.screen(:confirm) do |prompt|
|
748
|
-
prompt.yes? "Order #{quantity}x #{product} for $#{total}. Confirm?"
|
749
|
-
end
|
750
|
-
|
751
|
-
if confirmed
|
752
|
-
process_order(product, quantity)
|
753
|
-
app.say "Order placed successfully!"
|
137
|
+
create_account(email)
|
138
|
+
app.say "Account created successfully!"
|
754
139
|
else
|
755
|
-
app.say "
|
756
|
-
end
|
757
|
-
end
|
758
|
-
end
|
759
|
-
```
|
760
|
-
|
761
|
-
### Error Handling
|
762
|
-
|
763
|
-
Handle validation errors gracefully:
|
764
|
-
|
765
|
-
```ruby
|
766
|
-
app.screen(:credit_card) do |prompt|
|
767
|
-
prompt.ask "Enter credit card number:",
|
768
|
-
validate: ->(input) {
|
769
|
-
return "Card number must be 16 digits" unless input.length == 16
|
770
|
-
return "Invalid card number" unless luhn_valid?(input)
|
771
|
-
nil # Return nil for valid input
|
772
|
-
}
|
773
|
-
end
|
774
|
-
```
|
775
|
-
|
776
|
-
### Validation Error Display
|
777
|
-
|
778
|
-
Configure how validation errors are displayed to users:
|
779
|
-
|
780
|
-
```ruby
|
781
|
-
# config/initializers/flowchat.rb
|
782
|
-
|
783
|
-
# Default behavior: combine error with original message
|
784
|
-
FlowChat::Config.combine_validation_error_with_message = true
|
785
|
-
# User sees: "Card number must be 16 digits\n\nEnter credit card number:"
|
786
|
-
|
787
|
-
# Show only the error message
|
788
|
-
FlowChat::Config.combine_validation_error_with_message = false
|
789
|
-
# User sees: "Card number must be 16 digits"
|
790
|
-
```
|
791
|
-
|
792
|
-
**Use cases for each approach:**
|
793
|
-
|
794
|
-
- **Combined (default)**: Better for first-time users who need context about what they're entering
|
795
|
-
- **Error only**: Cleaner UX for experienced users, reduces message length for USSD character limits
|
796
|
-
|
797
|
-
**Example with both approaches:**
|
798
|
-
|
799
|
-
```ruby
|
800
|
-
# This validation code works the same way regardless of config
|
801
|
-
age = app.screen(:age) do |prompt|
|
802
|
-
prompt.ask "How old are you?",
|
803
|
-
convert: ->(input) { input.to_i },
|
804
|
-
validate: ->(input) { "You must be at least 18 years old" unless input >= 18 }
|
805
|
-
end
|
806
|
-
|
807
|
-
# With combine_validation_error_with_message = true (default):
|
808
|
-
# "You must be at least 18 years old
|
809
|
-
#
|
810
|
-
# How old are you?"
|
811
|
-
|
812
|
-
# With combine_validation_error_with_message = false:
|
813
|
-
# "You must be at least 18 years old"
|
814
|
-
```
|
815
|
-
|
816
|
-
### Background Job Support
|
817
|
-
|
818
|
-
For high-volume WhatsApp applications, use background response delivery:
|
819
|
-
|
820
|
-
```ruby
|
821
|
-
# app/jobs/whatsapp_message_job.rb
|
822
|
-
class WhatsappMessageJob < ApplicationJob
|
823
|
-
include FlowChat::Whatsapp::SendJobSupport
|
824
|
-
|
825
|
-
def perform(send_data)
|
826
|
-
perform_whatsapp_send(send_data)
|
827
|
-
end
|
828
|
-
end
|
829
|
-
|
830
|
-
# config/initializers/flowchat.rb
|
831
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
832
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
833
|
-
|
834
|
-
# config/application.rb
|
835
|
-
config.active_job.queue_adapter = :sidekiq
|
836
|
-
```
|
837
|
-
|
838
|
-
**The `SendJobSupport` module provides:**
|
839
|
-
- โ
**Automatic config resolution** - Resolves named configurations automatically
|
840
|
-
- โ
**Response delivery** - Handles sending responses to WhatsApp
|
841
|
-
- โ
**Error handling** - Comprehensive error handling with user notifications
|
842
|
-
- โ
**Retry logic** - Built-in exponential backoff retry
|
843
|
-
- โ
**Extensible** - Override methods for custom behavior
|
844
|
-
|
845
|
-
**How it works:**
|
846
|
-
1. **Controller receives webhook** - WhatsApp message arrives
|
847
|
-
2. **Flow processes synchronously** - Maintains controller context and session state
|
848
|
-
3. **Response queued for delivery** - Only the sending is moved to background
|
849
|
-
4. **Job sends response** - Background job handles API call to WhatsApp
|
850
|
-
|
851
|
-
**Advanced job with custom callbacks:**
|
852
|
-
|
853
|
-
```ruby
|
854
|
-
class AdvancedWhatsappMessageJob < ApplicationJob
|
855
|
-
include FlowChat::Whatsapp::SendJobSupport
|
856
|
-
|
857
|
-
def perform(send_data)
|
858
|
-
perform_whatsapp_send(send_data)
|
859
|
-
end
|
860
|
-
|
861
|
-
private
|
862
|
-
|
863
|
-
# Override for custom success handling
|
864
|
-
def on_whatsapp_send_success(send_data, result)
|
865
|
-
Rails.logger.info "Successfully sent WhatsApp message to #{send_data[:msisdn]}"
|
866
|
-
UserEngagementTracker.track_message_sent(phone: send_data[:msisdn])
|
867
|
-
end
|
868
|
-
|
869
|
-
# Override for custom error handling
|
870
|
-
def on_whatsapp_send_error(error, send_data)
|
871
|
-
ErrorTracker.notify(error, user_phone: send_data[:msisdn])
|
872
|
-
end
|
873
|
-
end
|
874
|
-
```
|
875
|
-
|
876
|
-
๐ก **See [examples/whatsapp_message_job.rb](examples/whatsapp_message_job.rb) for complete job implementation examples.**
|
877
|
-
|
878
|
-
### Middleware Configuration
|
879
|
-
|
880
|
-
FlowChat uses a **middleware architecture** to process USSD requests through a configurable pipeline. Each request flows through multiple middleware layers in a specific order.
|
881
|
-
|
882
|
-
#### Default Middleware Stack
|
883
|
-
|
884
|
-
When you run a flow, FlowChat automatically builds this middleware stack:
|
885
|
-
|
886
|
-
```
|
887
|
-
User Input โ Gateway โ Session โ Pagination โ Custom Middleware โ Executor โ Flow
|
888
|
-
```
|
889
|
-
|
890
|
-
1. **Gateway Middleware** - Handles USSD provider communication (Nalo)
|
891
|
-
2. **Session Middleware** - Manages session storage and retrieval
|
892
|
-
3. **Pagination Middleware** - Automatically splits long responses across pages
|
893
|
-
4. **Custom Middleware** - Your application-specific middleware (optional)
|
894
|
-
5. **Executor Middleware** - Executes the actual flow logic
|
895
|
-
|
896
|
-
#### Basic Configuration
|
897
|
-
|
898
|
-
```ruby
|
899
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
900
|
-
# Gateway configuration (required)
|
901
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
902
|
-
|
903
|
-
# Session storage (required)
|
904
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
905
|
-
|
906
|
-
# Add custom middleware (optional)
|
907
|
-
config.use_middleware MyLoggingMiddleware
|
908
|
-
|
909
|
-
# Enable resumable sessions (optional)
|
910
|
-
config.use_resumable_sessions
|
911
|
-
end
|
912
|
-
```
|
913
|
-
|
914
|
-
#### Runtime Middleware Modification
|
915
|
-
|
916
|
-
You can modify the middleware stack at runtime for advanced use cases:
|
917
|
-
|
918
|
-
```ruby
|
919
|
-
processor.run(MyFlow, :main_page) do |stack|
|
920
|
-
# Add authentication middleware
|
921
|
-
stack.use AuthenticationMiddleware
|
922
|
-
|
923
|
-
# Insert rate limiting before execution
|
924
|
-
stack.insert_before FlowChat::Ussd::Middleware::Executor, RateLimitMiddleware
|
925
|
-
|
926
|
-
# Add logging after gateway
|
927
|
-
stack.insert_after gateway, RequestLoggingMiddleware
|
928
|
-
end
|
929
|
-
```
|
930
|
-
|
931
|
-
#### Built-in Middleware
|
932
|
-
|
933
|
-
**Pagination Middleware** automatically handles responses longer than 182 characters (configurable):
|
934
|
-
|
935
|
-
```ruby
|
936
|
-
# Configure pagination behavior
|
937
|
-
FlowChat::Config.ussd.pagination_page_size = 140 # Default: 140 characters
|
938
|
-
FlowChat::Config.ussd.pagination_next_option = "#" # Default: "#"
|
939
|
-
FlowChat::Config.ussd.pagination_next_text = "More" # Default: "More"
|
940
|
-
FlowChat::Config.ussd.pagination_back_option = "0" # Default: "0"
|
941
|
-
FlowChat::Config.ussd.pagination_back_text = "Back" # Default: "Back"
|
942
|
-
```
|
943
|
-
|
944
|
-
**Resumable Sessions** allow users to continue interrupted conversations:
|
945
|
-
|
946
|
-
```ruby
|
947
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
948
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
949
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
950
|
-
config.use_resumable_sessions # Enable resumable sessions
|
951
|
-
end
|
952
|
-
```
|
953
|
-
|
954
|
-
#### Creating Custom Middleware
|
955
|
-
|
956
|
-
```ruby
|
957
|
-
class LoggingMiddleware
|
958
|
-
def initialize(app)
|
959
|
-
@app = app
|
960
|
-
end
|
961
|
-
|
962
|
-
def call(context)
|
963
|
-
Rails.logger.info "Processing USSD request: #{context.input}"
|
964
|
-
|
965
|
-
# Call the next middleware in the stack
|
966
|
-
result = @app.call(context)
|
967
|
-
|
968
|
-
Rails.logger.info "Response: #{result[1]}"
|
969
|
-
result
|
970
|
-
end
|
971
|
-
end
|
972
|
-
|
973
|
-
# Use your custom middleware
|
974
|
-
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
975
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
976
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
977
|
-
config.use_middleware LoggingMiddleware
|
978
|
-
end
|
979
|
-
```
|
980
|
-
|
981
|
-
### Multiple Gateways
|
982
|
-
|
983
|
-
FlowChat supports multiple USSD gateways:
|
984
|
-
|
985
|
-
```ruby
|
986
|
-
# Nalo Solutions Gateway
|
987
|
-
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
988
|
-
```
|
989
|
-
|
990
|
-
## Testing
|
991
|
-
|
992
|
-
FlowChat provides comprehensive testing capabilities for both USSD and WhatsApp flows. This section covers all testing approaches from simple unit tests to complex integration scenarios.
|
993
|
-
|
994
|
-
### Testing Approaches Overview
|
995
|
-
|
996
|
-
FlowChat supports two main testing strategies:
|
997
|
-
|
998
|
-
**๐ฏ Option 1: Simulator Mode (Recommended)**
|
999
|
-
- Bypasses WhatsApp API entirely
|
1000
|
-
- No webhook signature validation required
|
1001
|
-
- Responses returned as JSON instead of sent to WhatsApp
|
1002
|
-
- Perfect for unit and integration testing
|
1003
|
-
- Works with built-in web simulator interface
|
1004
|
-
|
1005
|
-
**๐ Option 2: Skip Signature Validation**
|
1006
|
-
- Tests real webhook endpoints without security complexity
|
1007
|
-
- Useful for staging environments
|
1008
|
-
- Set `skip_signature_validation = true`
|
1009
|
-
|
1010
|
-
### Environment-Specific Testing Configuration
|
1011
|
-
|
1012
|
-
Configure testing behavior per environment:
|
1013
|
-
|
1014
|
-
```ruby
|
1015
|
-
# config/initializers/flowchat.rb
|
1016
|
-
case Rails.env
|
1017
|
-
when 'development'
|
1018
|
-
# Enable simulator mode for testing
|
1019
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
1020
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
1021
|
-
|
1022
|
-
when 'test'
|
1023
|
-
# Enable simulator mode for automated testing
|
1024
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
1025
|
-
FlowChat::Config.simulator_secret = "test_secret"
|
1026
|
-
|
1027
|
-
when 'staging'
|
1028
|
-
# Use inline mode but allow simulator for testing
|
1029
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
1030
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
1031
|
-
|
1032
|
-
when 'production'
|
1033
|
-
# Background processing in production, no simulator
|
1034
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
1035
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
1036
|
-
FlowChat::Config.simulator_secret = nil
|
1037
|
-
end
|
1038
|
-
```
|
1039
|
-
|
1040
|
-
### Unit Testing Flows
|
1041
|
-
|
1042
|
-
Test your flows in isolation using the provided test helpers:
|
1043
|
-
|
1044
|
-
```ruby
|
1045
|
-
require 'test_helper'
|
1046
|
-
|
1047
|
-
class WelcomeFlowTest < Minitest::Test
|
1048
|
-
def setup
|
1049
|
-
@context = FlowChat::Context.new
|
1050
|
-
@context.session = create_test_session_store
|
1051
|
-
end
|
1052
|
-
|
1053
|
-
def test_welcome_flow_with_name
|
1054
|
-
@context.input = "John Doe"
|
1055
|
-
app = FlowChat::Ussd::App.new(@context)
|
1056
|
-
|
1057
|
-
error = assert_raises(FlowChat::Interrupt::Terminate) do
|
1058
|
-
flow = WelcomeFlow.new(app)
|
1059
|
-
flow.main_page
|
140
|
+
app.say "Registration cancelled."
|
1060
141
|
end
|
1061
|
-
|
1062
|
-
assert_equal "Hello, John Doe! Welcome to FlowChat.", error.prompt
|
1063
142
|
end
|
1064
143
|
end
|
1065
144
|
```
|
1066
145
|
|
1067
|
-
###
|
1068
|
-
|
1069
|
-
#### Simulator Mode Testing
|
1070
|
-
|
1071
|
-
```ruby
|
1072
|
-
test "complete flow via simulator" do
|
1073
|
-
# Generate valid simulator cookie for authentication
|
1074
|
-
valid_cookie = generate_simulator_cookie
|
1075
|
-
|
1076
|
-
webhook_payload = {
|
1077
|
-
entry: [{ changes: [{ value: { messages: [{ from: "1234567890", text: { body: "Hello" }, type: "text" }] } }] }],
|
1078
|
-
simulator_mode: true
|
1079
|
-
}
|
1080
|
-
|
1081
|
-
post "/whatsapp/webhook", params: webhook_payload, cookies: { flowchat_simulator: valid_cookie }
|
1082
|
-
assert_response :success
|
1083
|
-
end
|
1084
|
-
```
|
1085
|
-
|
1086
|
-
#### Testing with Skipped Validation
|
1087
|
-
|
1088
|
-
```ruby
|
1089
|
-
test "webhook processing with skipped validation" do
|
1090
|
-
config = FlowChat::Whatsapp::Configuration.new
|
1091
|
-
config.skip_signature_validation = true # Skip for testing
|
1092
|
-
|
1093
|
-
# No signature required when validation is skipped
|
1094
|
-
post "/whatsapp/webhook", params: webhook_payload.to_json
|
1095
|
-
assert_response :success
|
1096
|
-
end
|
1097
|
-
```
|
1098
|
-
|
1099
|
-
### Test Helper Methods
|
1100
|
-
|
1101
|
-
```ruby
|
1102
|
-
private
|
146
|
+
### Input Methods
|
1103
147
|
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
def generate_simulator_cookie(secret = FlowChat::Config.simulator_secret)
|
1109
|
-
timestamp = Time.now.to_i
|
1110
|
-
message = "simulator:#{timestamp}"
|
1111
|
-
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, message)
|
1112
|
-
"#{timestamp}:#{signature}"
|
1113
|
-
end
|
1114
|
-
```
|
148
|
+
- **`prompt.ask()`** - Free-form input with optional validation
|
149
|
+
- **`prompt.select()`** - Force selection from predefined options
|
150
|
+
- **`prompt.yes?()`** - Simple yes/no questions
|
1115
151
|
|
1116
|
-
##
|
152
|
+
## WhatsApp Client
|
1117
153
|
|
1118
|
-
|
154
|
+
Send messages outside of flows:
|
1119
155
|
|
1120
156
|
```ruby
|
1121
|
-
|
1122
|
-
|
1123
|
-
# Core configuration
|
1124
|
-
FlowChat::Config.logger = Rails.logger
|
1125
|
-
FlowChat::Config.cache = Rails.cache
|
1126
|
-
FlowChat::Config.simulator_secret = "your_secure_secret_here"
|
1127
|
-
|
1128
|
-
# Validation error display behavior
|
1129
|
-
# When true (default), validation errors are combined with the original message.
|
1130
|
-
# When false, only the validation error message is shown to the user.
|
1131
|
-
FlowChat::Config.combine_validation_error_with_message = true
|
1132
|
-
|
1133
|
-
# USSD configuration
|
1134
|
-
FlowChat::Config.ussd.pagination_page_size = 140
|
1135
|
-
FlowChat::Config.ussd.pagination_next_option = "#"
|
1136
|
-
FlowChat::Config.ussd.pagination_next_text = "More"
|
1137
|
-
FlowChat::Config.ussd.pagination_back_option = "0"
|
1138
|
-
FlowChat::Config.ussd.pagination_back_text = "Back"
|
1139
|
-
FlowChat::Config.ussd.resumable_sessions_enabled = true
|
1140
|
-
FlowChat::Config.ussd.resumable_sessions_timeout_seconds = 300
|
1141
|
-
|
1142
|
-
# WhatsApp configuration
|
1143
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
|
1144
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
1145
|
-
```
|
1146
|
-
|
1147
|
-
### WhatsApp Security Configuration
|
1148
|
-
|
1149
|
-
```ruby
|
1150
|
-
# Per-configuration security settings
|
1151
|
-
config = FlowChat::Whatsapp::Configuration.new
|
1152
|
-
config.app_secret = "your_whatsapp_app_secret" # Required for signature validation
|
1153
|
-
config.skip_signature_validation = false # Default: false (enforce validation)
|
1154
|
-
|
1155
|
-
# Security modes:
|
1156
|
-
# 1. Full security (production)
|
1157
|
-
config.app_secret = "secret"
|
1158
|
-
config.skip_signature_validation = false
|
1159
|
-
|
1160
|
-
# 2. Development mode (disable validation)
|
1161
|
-
config.app_secret = nil
|
1162
|
-
config.skip_signature_validation = true
|
1163
|
-
```
|
1164
|
-
|
1165
|
-
## Best Practices
|
1166
|
-
|
1167
|
-
### 1. Security Best Practices
|
1168
|
-
|
1169
|
-
**Production Security Checklist:**
|
1170
|
-
|
1171
|
-
โ
**Always configure app_secret** for webhook validation
|
1172
|
-
```ruby
|
1173
|
-
config.app_secret = "your_whatsapp_app_secret" # Never leave empty in production
|
1174
|
-
config.skip_signature_validation = false # Never disable in production
|
1175
|
-
```
|
1176
|
-
|
1177
|
-
โ
**Use secure simulator secrets**
|
1178
|
-
```ruby
|
1179
|
-
# Use Rails secret_key_base + suffix for uniqueness
|
1180
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
1181
|
-
|
1182
|
-
# Or use environment variables
|
1183
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
1184
|
-
```
|
1185
|
-
|
1186
|
-
โ
**Environment-specific configuration**
|
1187
|
-
```ruby
|
1188
|
-
# Different security levels per environment
|
1189
|
-
if Rails.env.production?
|
1190
|
-
config.skip_signature_validation = false # Enforce validation
|
1191
|
-
else
|
1192
|
-
config.skip_signature_validation = true # Allow for development
|
1193
|
-
end
|
1194
|
-
```
|
1195
|
-
|
1196
|
-
โ
**Enable simulator only when needed**
|
1197
|
-
```ruby
|
1198
|
-
# Only enable simulator in development/staging (default)
|
1199
|
-
enable_simulator = Rails.env.development? || Rails.env.staging?
|
1200
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: enable_simulator)
|
1201
|
-
```
|
1202
|
-
|
1203
|
-
### 5. Choose the Right WhatsApp Mode
|
1204
|
-
|
1205
|
-
Configure the appropriate mode based on your environment and requirements:
|
1206
|
-
|
1207
|
-
```ruby
|
1208
|
-
# config/initializers/flowchat.rb
|
1209
|
-
|
1210
|
-
# Development/Testing - use simulator mode with security
|
1211
|
-
if Rails.env.development? || Rails.env.test?
|
1212
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
1213
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
1214
|
-
end
|
1215
|
-
|
1216
|
-
# Staging - use inline mode with full security
|
1217
|
-
if Rails.env.staging?
|
1218
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
1219
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
1220
|
-
end
|
1221
|
-
|
1222
|
-
# Production - use background mode with full security
|
1223
|
-
if Rails.env.production?
|
1224
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
1225
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
1226
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
1227
|
-
end
|
1228
|
-
```
|
1229
|
-
|
1230
|
-
### 6. Error Handling Best Practices
|
1231
|
-
|
1232
|
-
Handle security and configuration errors gracefully:
|
1233
|
-
|
1234
|
-
```ruby
|
1235
|
-
def webhook
|
1236
|
-
begin
|
1237
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
|
1238
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
1239
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
1240
|
-
end
|
157
|
+
config = FlowChat::Whatsapp::Configuration.from_credentials
|
158
|
+
client = FlowChat::Whatsapp::Client.new(config)
|
1241
159
|
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
rescue => e
|
1249
|
-
Rails.logger.error "Unexpected error processing WhatsApp webhook: #{e.message}"
|
1250
|
-
head :internal_server_error
|
1251
|
-
end
|
1252
|
-
end
|
160
|
+
# Send messages
|
161
|
+
client.send_text("+1234567890", "Hello!")
|
162
|
+
client.send_buttons("+1234567890", "Choose:", [
|
163
|
+
{ id: 'option1', title: 'Option 1' },
|
164
|
+
{ id: 'option2', title: 'Option 2' }
|
165
|
+
])
|
1253
166
|
```
|
1254
167
|
|
1255
|
-
##
|
1256
|
-
|
1257
|
-
### Context Variables Reference
|
1258
|
-
|
1259
|
-
FlowChat provides a rich set of context variables that are available throughout your flows. These variables are automatically populated based on the gateway and message type.
|
1260
|
-
|
1261
|
-
#### Core Context Variables
|
1262
|
-
|
1263
|
-
**Request Variables**
|
1264
|
-
|
1265
|
-
| Variable | Description |
|
1266
|
-
|----------|-------------|
|
1267
|
-
| `context.input` | Current user input |
|
1268
|
-
| `context["request.id"]` | Unique request ID |
|
1269
|
-
| `context["request.timestamp"]` | Request timestamp |
|
1270
|
-
| `context["request.msisdn"]` | User's phone number |
|
1271
|
-
| `context["request.gateway"]` | Current gateway (`:whatsapp_cloud_api`, `:ussd_nalo`) |
|
1272
|
-
| `context["enable_simulator"]` | Whether simulator mode is enabled for this request |
|
1273
|
-
| `context["simulator_mode"]` | Whether simulator mode is active |
|
1274
|
-
|
1275
|
-
**Flow Variables**
|
1276
|
-
|
1277
|
-
| Variable | Description |
|
1278
|
-
|----------|-------------|
|
1279
|
-
| `context["flow.name"]` | Current flow name |
|
1280
|
-
| `context["flow.class"]` | Current flow class |
|
1281
|
-
| `context["flow.action"]` | Current flow action/method |
|
1282
|
-
|
1283
|
-
**Session Variables**
|
1284
|
-
|
1285
|
-
| Variable | Description |
|
1286
|
-
|----------|-------------|
|
1287
|
-
| `context["session.id"]` | Current session ID |
|
1288
|
-
| `context["session.store"]` | Session data store |
|
1289
|
-
|
1290
|
-
**Controller Variables**
|
1291
|
-
|
1292
|
-
| Variable | Description |
|
1293
|
-
|----------|-------------|
|
1294
|
-
| `context.controller` | Current controller instance |
|
168
|
+
## Testing Simulator
|
1295
169
|
|
1296
|
-
|
170
|
+
FlowChat includes a powerful built-in simulator with a modern web interface for testing both USSD and WhatsApp flows:
|
1297
171
|
|
1298
|
-
|
1299
|
-
|----------|-------------|
|
1300
|
-
| `context["whatsapp.message_result"]` | Result of last message send |
|
1301
|
-
| `context["whatsapp.message_id"]` | WhatsApp message ID |
|
1302
|
-
| `context["whatsapp.contact_name"]` | WhatsApp contact name |
|
1303
|
-
| `context["whatsapp.location"]` | Location data if shared |
|
1304
|
-
| `context["whatsapp.media"]` | Media data if attached |
|
172
|
+

|
1305
173
|
|
1306
|
-
|
174
|
+
Features:
|
175
|
+
- ๐ฑ **Visual Interface** - Phone-like display with real conversation flow
|
176
|
+
- ๐ **Environment Switching** - Toggle between USSD and WhatsApp modes
|
177
|
+
- ๐ **Request Logging** - Real-time HTTP request/response monitoring
|
178
|
+
- ๐ฏ **Interactive Testing** - Test flows with character counting and validation
|
179
|
+
- ๐ ๏ธ **Developer Tools** - Session management and connection status
|
1307
180
|
|
1308
|
-
|
1309
|
-
|----------|-------------|
|
1310
|
-
| `context["ussd.request"]` | Original USSD request |
|
1311
|
-
| `context["ussd.response"]` | USSD response object |
|
1312
|
-
| `context["ussd.pagination"]` | Pagination data for long messages |
|
181
|
+
See the [Testing Guide](docs/testing.md) for complete setup instructions and testing strategies.
|
1313
182
|
|
1314
|
-
#### Session Data Variables
|
1315
183
|
|
1316
|
-
|
1317
|
-
|----------|-------------|
|
1318
|
-
| `context["$started_at$"]` | When the conversation started |
|
184
|
+
## Documentation
|
1319
185
|
|
1320
|
-
|
186
|
+
- **[WhatsApp Setup](docs/whatsapp-setup.md)** - Comprehensive WhatsApp configuration
|
187
|
+
- **[USSD Setup](docs/ussd-setup.md)** - USSD gateway configuration and examples
|
188
|
+
- **[Flow Development](docs/flows.md)** - Advanced flow patterns and techniques
|
189
|
+
- **[Media Support](docs/media.md)** - Rich media handling for WhatsApp
|
190
|
+
- **[Testing Guide](docs/testing.md)** - Complete testing strategies
|
191
|
+
- **[Configuration Reference](docs/configuration.md)** - All configuration options
|
192
|
+
- **[Instrumentation](docs/instrumentation.md)** - Monitoring and metrics
|
193
|
+
- **[Security](docs/security.md)** - Security best practices
|
194
|
+
- **[Examples](examples/)** - Complete working examples
|
1321
195
|
|
1322
|
-
|
1323
|
-
```ruby
|
1324
|
-
# Direct access
|
1325
|
-
context.input
|
1326
|
-
context["request.msisdn"]
|
1327
|
-
|
1328
|
-
# Through app object in flows
|
1329
|
-
app.phone_number
|
1330
|
-
app.contact_name
|
1331
|
-
app.message_id
|
1332
|
-
```
|
196
|
+
## Examples
|
1333
197
|
|
1334
|
-
|
1335
|
-
- WhatsApp variables are only available when using WhatsApp gateway
|
1336
|
-
- USSD variables are only available when using USSD gateway
|
1337
|
-
- Core variables are available across all gateways
|
198
|
+
See the [examples directory](examples/) for complete implementations:
|
1338
199
|
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
200
|
+
- **[USSD Controller](examples/ussd_controller.rb)** - Full USSD implementation
|
201
|
+
- **[WhatsApp Controller](examples/whatsapp_controller.rb)** - Basic WhatsApp setup
|
202
|
+
- **[Multi-tenant WhatsApp](examples/multi_tenant_whatsapp_controller.rb)** - Advanced WhatsApp
|
203
|
+
- **[Background Jobs](examples/whatsapp_message_job.rb)** - Async message processing
|
204
|
+
- **[Simulator Controller](examples/simulator_controller.rb)** - Testing setup
|
1344
205
|
|
1345
|
-
|
1346
|
-
- Webhook signatures are validated for WhatsApp requests
|
1347
|
-
- Simulator mode requires valid simulator cookie
|
1348
|
-
- Session data is encrypted at rest
|
206
|
+
## License
|
1349
207
|
|
1350
|
-
|
1351
|
-
- Context variables can be used to control flow logic
|
1352
|
-
- Session data can be used to maintain state
|
1353
|
-
- Request data can be used for validation
|
208
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|