flow_chat 0.6.1 → 0.8.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 +85 -1229
- data/docs/configuration.md +360 -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/sessions.md +433 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +322 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +13 -22
- data/examples/ussd_controller.rb +41 -41
- 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 +79 -2
- data/lib/flow_chat/config.rb +31 -5
- 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 +130 -12
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +8 -8
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
- 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 +16 -4
- 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 +121 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +7 -1
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +23 -12
- 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/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
@@ -0,0 +1,360 @@
|
|
1
|
+
# Configuration Reference
|
2
|
+
|
3
|
+
This document covers all FlowChat configuration options.
|
4
|
+
|
5
|
+
## Framework Configuration
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# config/initializers/flowchat.rb
|
9
|
+
|
10
|
+
# Core configuration
|
11
|
+
FlowChat::Config.logger = Rails.logger
|
12
|
+
FlowChat::Config.cache = Rails.cache
|
13
|
+
FlowChat::Config.simulator_secret = "your_secure_secret_here"
|
14
|
+
|
15
|
+
# Validation error display behavior
|
16
|
+
FlowChat::Config.combine_validation_error_with_message = true # default
|
17
|
+
|
18
|
+
# Setup instrumentation (optional)
|
19
|
+
FlowChat.setup_instrumentation!
|
20
|
+
```
|
21
|
+
|
22
|
+
## Session Configuration
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
# Session boundaries control how session IDs are constructed
|
26
|
+
FlowChat::Config.session.boundaries = [:flow, :platform] # default
|
27
|
+
FlowChat::Config.session.hash_phone_numbers = true # hash phone numbers for privacy
|
28
|
+
FlowChat::Config.session.identifier = nil # let platforms choose (default)
|
29
|
+
|
30
|
+
# Available boundary options:
|
31
|
+
# :flow - separate sessions per flow class
|
32
|
+
# :platform - separate sessions per platform (ussd, whatsapp)
|
33
|
+
# :gateway - separate sessions per gateway
|
34
|
+
# [] - global sessions (no boundaries)
|
35
|
+
|
36
|
+
# Available identifier options:
|
37
|
+
# nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
|
38
|
+
# :msisdn - use phone number (durable sessions)
|
39
|
+
# :request_id - use request ID (ephemeral sessions)
|
40
|
+
```
|
41
|
+
|
42
|
+
## USSD Configuration
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# USSD pagination settings
|
46
|
+
FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
|
47
|
+
FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next page
|
48
|
+
FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
|
49
|
+
FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
|
50
|
+
FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
|
51
|
+
```
|
52
|
+
|
53
|
+
## WhatsApp Configuration
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# Message handling modes
|
57
|
+
FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
|
58
|
+
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
59
|
+
```
|
60
|
+
|
61
|
+
### WhatsApp Credential Configuration
|
62
|
+
|
63
|
+
#### Option 1: Rails Credentials
|
64
|
+
|
65
|
+
```bash
|
66
|
+
rails credentials:edit
|
67
|
+
```
|
68
|
+
|
69
|
+
```yaml
|
70
|
+
whatsapp:
|
71
|
+
access_token: "your_access_token"
|
72
|
+
phone_number_id: "your_phone_number_id"
|
73
|
+
verify_token: "your_verify_token"
|
74
|
+
app_id: "your_app_id"
|
75
|
+
app_secret: "your_app_secret"
|
76
|
+
business_account_id: "your_business_account_id"
|
77
|
+
skip_signature_validation: false
|
78
|
+
```
|
79
|
+
|
80
|
+
#### Option 2: Environment Variables
|
81
|
+
|
82
|
+
```bash
|
83
|
+
export WHATSAPP_ACCESS_TOKEN="your_access_token"
|
84
|
+
export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
|
85
|
+
export WHATSAPP_VERIFY_TOKEN="your_verify_token"
|
86
|
+
export WHATSAPP_APP_ID="your_app_id"
|
87
|
+
export WHATSAPP_APP_SECRET="your_app_secret"
|
88
|
+
export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
|
89
|
+
export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
|
90
|
+
```
|
91
|
+
|
92
|
+
#### Option 3: Programmatic Configuration
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
config = FlowChat::Whatsapp::Configuration.new(:my_config) # Named configuration
|
96
|
+
config.access_token = "your_access_token"
|
97
|
+
config.phone_number_id = "your_phone_number_id"
|
98
|
+
config.verify_token = "your_verify_token"
|
99
|
+
config.app_id = "your_app_id"
|
100
|
+
config.app_secret = "your_app_secret"
|
101
|
+
config.business_account_id = "your_business_account_id"
|
102
|
+
config.skip_signature_validation = false
|
103
|
+
# Configuration is automatically registered as :my_config
|
104
|
+
```
|
105
|
+
|
106
|
+
**⚠️ Important for Background Jobs:** When using background mode with programmatic configurations, you must register them in an initializer:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# config/initializers/whatsapp_configs.rb
|
110
|
+
# Register configurations so background jobs can access them
|
111
|
+
production_config = FlowChat::Whatsapp::Configuration.new(:production)
|
112
|
+
production_config.access_token = ENV['PROD_WHATSAPP_TOKEN']
|
113
|
+
# ... other settings
|
114
|
+
|
115
|
+
staging_config = FlowChat::Whatsapp::Configuration.new(:staging)
|
116
|
+
staging_config.access_token = ENV['STAGING_WHATSAPP_TOKEN']
|
117
|
+
# ... other settings
|
118
|
+
```
|
119
|
+
|
120
|
+
Then use named configurations in controllers:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# Use registered configuration
|
124
|
+
config = FlowChat::Whatsapp::Configuration.get(:production)
|
125
|
+
processor = FlowChat::Whatsapp::Processor.new(self) do |config|
|
126
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
## Security Configuration
|
131
|
+
|
132
|
+
### WhatsApp Security
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# Production security (recommended)
|
136
|
+
config.app_secret = "your_whatsapp_app_secret"
|
137
|
+
config.skip_signature_validation = false # default
|
138
|
+
|
139
|
+
# Development mode (disable validation)
|
140
|
+
config.app_secret = nil
|
141
|
+
config.skip_signature_validation = true
|
142
|
+
```
|
143
|
+
|
144
|
+
### Simulator Security
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
# Use Rails secret for uniqueness
|
148
|
+
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
|
149
|
+
|
150
|
+
# Or use dedicated secret
|
151
|
+
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
152
|
+
```
|
153
|
+
|
154
|
+
## Environment-Specific Configuration
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
# config/initializers/flowchat.rb
|
158
|
+
case Rails.env
|
159
|
+
when 'development'
|
160
|
+
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
161
|
+
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
162
|
+
|
163
|
+
when 'test'
|
164
|
+
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
165
|
+
FlowChat::Config.simulator_secret = "test_secret"
|
166
|
+
|
167
|
+
when 'staging'
|
168
|
+
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
169
|
+
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
170
|
+
|
171
|
+
when 'production'
|
172
|
+
FlowChat::Config.whatsapp.message_handling_mode = :background
|
173
|
+
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
174
|
+
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
## Processor Configuration
|
179
|
+
|
180
|
+
### USSD Processor
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
184
|
+
# Gateway (required)
|
185
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
186
|
+
|
187
|
+
# Session storage (required)
|
188
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
189
|
+
|
190
|
+
# Optional middleware
|
191
|
+
config.use_middleware MyCustomMiddleware
|
192
|
+
|
193
|
+
# Configure session boundaries
|
194
|
+
config.use_session_config(
|
195
|
+
boundaries: [:flow, :platform], # which boundaries to enforce
|
196
|
+
hash_phone_numbers: true, # hash phone numbers for privacy
|
197
|
+
identifier: :msisdn # use MSISDN for durable sessions (optional)
|
198
|
+
)
|
199
|
+
|
200
|
+
# Shorthand for durable sessions (identifier: :msisdn)
|
201
|
+
config.use_durable_sessions
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
### WhatsApp Processor
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
|
209
|
+
# Gateway (required)
|
210
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
211
|
+
|
212
|
+
# Session storage (required)
|
213
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
214
|
+
|
215
|
+
# Optional custom configuration
|
216
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_whatsapp_config
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
## Session Store Options
|
221
|
+
|
222
|
+
### Cache Session Store
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
226
|
+
```
|
227
|
+
|
228
|
+
Uses Rails cache backend with automatic TTL management. This is the primary session store available in FlowChat.
|
229
|
+
|
230
|
+
## Middleware Configuration
|
231
|
+
|
232
|
+
### Built-in Middleware
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
# Pagination (USSD only, automatic)
|
236
|
+
FlowChat::Ussd::Middleware::Pagination
|
237
|
+
|
238
|
+
# Session management (automatic)
|
239
|
+
FlowChat::Session::Middleware
|
240
|
+
|
241
|
+
# Gateway communication (automatic)
|
242
|
+
FlowChat::Ussd::Gateway::Nalo
|
243
|
+
FlowChat::Whatsapp::Gateway::CloudApi
|
244
|
+
```
|
245
|
+
|
246
|
+
### Custom Middleware
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class LoggingMiddleware
|
250
|
+
def initialize(app)
|
251
|
+
@app = app
|
252
|
+
end
|
253
|
+
|
254
|
+
def call(context)
|
255
|
+
Rails.logger.info "Processing request: #{context.input}"
|
256
|
+
result = @app.call(context)
|
257
|
+
Rails.logger.info "Response: #{result[1]}"
|
258
|
+
result
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Use custom middleware
|
263
|
+
config.use_middleware LoggingMiddleware
|
264
|
+
```
|
265
|
+
|
266
|
+
## Validation Configuration
|
267
|
+
|
268
|
+
### Error Display Options
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
# Combine validation error with original message (default)
|
272
|
+
FlowChat::Config.combine_validation_error_with_message = true
|
273
|
+
# User sees: "Invalid email format\n\nEnter your email:"
|
274
|
+
|
275
|
+
# Show only validation error
|
276
|
+
FlowChat::Config.combine_validation_error_with_message = false
|
277
|
+
# User sees: "Invalid email format"
|
278
|
+
```
|
279
|
+
|
280
|
+
## Background Job Configuration
|
281
|
+
|
282
|
+
### Job Class Setup
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
# app/jobs/whatsapp_message_job.rb
|
286
|
+
class WhatsappMessageJob < ApplicationJob
|
287
|
+
include FlowChat::Whatsapp::SendJobSupport
|
288
|
+
|
289
|
+
def perform(send_data)
|
290
|
+
perform_whatsapp_send(send_data)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
**Configuration Resolution:** The job automatically resolves configurations using:
|
296
|
+
1. Named configuration from `send_data[:configuration_name]` if present
|
297
|
+
2. Default configuration from credentials/environment variables
|
298
|
+
|
299
|
+
For custom resolution logic, override the configuration resolution:
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class CustomWhatsappMessageJob < ApplicationJob
|
303
|
+
include FlowChat::Whatsapp::SendJobSupport
|
304
|
+
|
305
|
+
def perform(send_data)
|
306
|
+
perform_whatsapp_send(send_data)
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
|
311
|
+
def resolve_whatsapp_configuration(send_data)
|
312
|
+
# Custom logic to resolve configuration
|
313
|
+
tenant_id = ...
|
314
|
+
FlowChat::Whatsapp::Configuration.get("tenant_#{tenant_id}")
|
315
|
+
end
|
316
|
+
end
|
317
|
+
```
|
318
|
+
|
319
|
+
### Queue Configuration
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
# config/application.rb
|
323
|
+
config.active_job.queue_adapter = :sidekiq
|
324
|
+
|
325
|
+
# config/initializers/flowchat.rb
|
326
|
+
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
327
|
+
```
|
328
|
+
|
329
|
+
## Instrumentation Configuration
|
330
|
+
|
331
|
+
### Basic Setup
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
# Enable instrumentation
|
335
|
+
FlowChat.setup_instrumentation!
|
336
|
+
```
|
337
|
+
|
338
|
+
### Custom Event Subscribers
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
# Subscribe to specific events
|
342
|
+
ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |event|
|
343
|
+
# Custom handling
|
344
|
+
ExternalMonitoring.track_flow_execution(
|
345
|
+
event.payload[:flow_name],
|
346
|
+
event.duration
|
347
|
+
)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Subscribe to all FlowChat events
|
351
|
+
ActiveSupport::Notifications.subscribe(/\.flow_chat$/) do |name, start, finish, id, payload|
|
352
|
+
CustomLogger.log_event(name, payload.merge(duration: finish - start))
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
## Configuration Validation
|
357
|
+
|
358
|
+
FlowChat validates configuration at runtime and provides helpful error messages:
|
359
|
+
|
360
|
+
FlowChat validates configuration at runtime and provides helpful error messages for missing or invalid configurations.
|
data/docs/flows.md
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
# Flow Development Guide
|
2
|
+
|
3
|
+
This guide covers advanced flow patterns, validation techniques, and best practices for building sophisticated conversational workflows.
|
4
|
+
|
5
|
+
## Flow Architecture
|
6
|
+
|
7
|
+
### Flow Lifecycle
|
8
|
+
|
9
|
+
Every flow method must result in user interaction:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class ExampleFlow < FlowChat::Flow
|
13
|
+
def main_page
|
14
|
+
# ✅ Always end with user interaction
|
15
|
+
choice = app.screen(:choice) { |p| p.select "Choose:", ["A", "B"] }
|
16
|
+
|
17
|
+
case choice
|
18
|
+
when "A"
|
19
|
+
handle_option_a
|
20
|
+
app.say "Option A completed!" # Required interaction
|
21
|
+
when "B"
|
22
|
+
handle_option_b
|
23
|
+
app.say "Option B completed!" # Required interaction
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
### Session Management
|
30
|
+
|
31
|
+
FlowChat automatically persists screen results:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class RegistrationFlow < FlowChat::Flow
|
35
|
+
def main_page
|
36
|
+
# These values persist across requests
|
37
|
+
name = app.screen(:name) { |p| p.ask "Name?" }
|
38
|
+
email = app.screen(:email) { |p| p.ask "Email?" }
|
39
|
+
|
40
|
+
# Show summary using cached values
|
41
|
+
confirmed = app.screen(:confirm) do |prompt|
|
42
|
+
prompt.yes? "Create account for #{name} (#{email})?"
|
43
|
+
end
|
44
|
+
|
45
|
+
if confirmed
|
46
|
+
create_user(name: name, email: email)
|
47
|
+
app.say "Account created!"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
## Input Validation Patterns
|
54
|
+
|
55
|
+
### Basic Validation
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
age = app.screen(:age) do |prompt|
|
59
|
+
prompt.ask "Enter your age:",
|
60
|
+
validate: ->(input) {
|
61
|
+
return "Age must be a number" unless input.match?(/^\d+$/)
|
62
|
+
return "Must be 18 or older" unless input.to_i >= 18
|
63
|
+
nil # Return nil for valid input
|
64
|
+
},
|
65
|
+
transform: ->(input) { input.to_i }
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Complex Validation
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
phone = app.screen(:phone) do |prompt|
|
73
|
+
prompt.ask "Enter phone number:",
|
74
|
+
validate: ->(input) {
|
75
|
+
clean = input.gsub(/[\s\-\(\)]/, '')
|
76
|
+
return "Invalid format" unless clean.match?(/^\+?[\d]{10,15}$/)
|
77
|
+
return "Must start with country code" unless clean.start_with?('+')
|
78
|
+
nil
|
79
|
+
},
|
80
|
+
transform: ->(input) { input.gsub(/[\s\-\(\)]/, '') }
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
### Conditional Validation
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class PaymentFlow < FlowChat::Flow
|
88
|
+
def collect_payment_method
|
89
|
+
method = app.screen(:method) do |prompt|
|
90
|
+
prompt.select "Payment method:", ["card", "mobile_money"]
|
91
|
+
end
|
92
|
+
|
93
|
+
if method == "card"
|
94
|
+
collect_card_details
|
95
|
+
else
|
96
|
+
collect_mobile_money_details
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def collect_card_details
|
103
|
+
card = app.screen(:card) do |prompt|
|
104
|
+
prompt.ask "Card number (16 digits):",
|
105
|
+
validate: ->(input) {
|
106
|
+
clean = input.gsub(/\s/, '')
|
107
|
+
return "Must be 16 digits" unless clean.length == 16
|
108
|
+
return "Invalid card number" unless luhn_valid?(clean)
|
109
|
+
nil
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
app.say "Card ending in #{card[-4..-1]} saved."
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
## Menu Patterns
|
119
|
+
|
120
|
+
### Dynamic Menus
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
def show_products
|
124
|
+
products = fetch_available_products
|
125
|
+
|
126
|
+
choice = app.screen(:product) do |prompt|
|
127
|
+
prompt.select "Choose product:", products.map(&:name)
|
128
|
+
end
|
129
|
+
|
130
|
+
selected_product = products.find { |p| p.name == choice }
|
131
|
+
show_product_details(selected_product)
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Nested Menus
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
def main_menu
|
139
|
+
choice = app.screen(:main) do |prompt|
|
140
|
+
prompt.select "Main Menu:", {
|
141
|
+
"products" => "View Products",
|
142
|
+
"orders" => "My Orders",
|
143
|
+
"support" => "Customer Support"
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
case choice
|
148
|
+
when "products"
|
149
|
+
products_menu
|
150
|
+
when "orders"
|
151
|
+
orders_menu
|
152
|
+
when "support"
|
153
|
+
support_menu
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
## Advanced Patterns
|
159
|
+
|
160
|
+
### Multi-Step Forms
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class CompleteProfileFlow < FlowChat::Flow
|
164
|
+
def main_page
|
165
|
+
collect_basic_info
|
166
|
+
collect_preferences
|
167
|
+
confirm_and_save
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def collect_basic_info
|
173
|
+
app.screen(:name) { |p| p.ask "Full name:" }
|
174
|
+
app.screen(:email) { |p| p.ask "Email:" }
|
175
|
+
app.screen(:phone) { |p| p.ask "Phone:" }
|
176
|
+
end
|
177
|
+
|
178
|
+
def collect_preferences
|
179
|
+
app.screen(:language) { |p| p.select "Language:", ["English", "French"] }
|
180
|
+
app.screen(:notifications) { |p| p.yes? "Enable notifications?" }
|
181
|
+
end
|
182
|
+
|
183
|
+
def confirm_and_save
|
184
|
+
summary = build_summary
|
185
|
+
confirmed = app.screen(:confirm) { |p| p.yes? "Save profile?\n\n#{summary}" }
|
186
|
+
|
187
|
+
if confirmed
|
188
|
+
save_profile
|
189
|
+
app.say "Profile saved successfully!"
|
190
|
+
else
|
191
|
+
app.say "Profile not saved."
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
### Error Recovery
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
def process_payment
|
201
|
+
begin
|
202
|
+
amount = app.screen(:amount) do |prompt|
|
203
|
+
prompt.ask "Amount to pay:",
|
204
|
+
validate: ->(input) {
|
205
|
+
return "Invalid amount" unless input.match?(/^\d+(\.\d{2})?$/)
|
206
|
+
return "Minimum $1.00" unless input.to_f >= 1.0
|
207
|
+
nil
|
208
|
+
},
|
209
|
+
transform: ->(input) { input.to_f }
|
210
|
+
end
|
211
|
+
|
212
|
+
process_transaction(amount)
|
213
|
+
app.say "Payment of $#{amount} processed successfully!"
|
214
|
+
|
215
|
+
rescue PaymentError => e
|
216
|
+
app.say "Payment failed: #{e.message}. Please try again."
|
217
|
+
process_payment # Retry
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
## Cross-Platform Considerations
|
223
|
+
|
224
|
+
### Platform Detection
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
def show_help
|
228
|
+
if app.context["request.gateway"] == :whatsapp_cloud_api
|
229
|
+
# WhatsApp users get rich media
|
230
|
+
app.say "Here's how to use our service:",
|
231
|
+
media: { type: :image, url: "https://example.com/help.jpg" }
|
232
|
+
else
|
233
|
+
# USSD users get text with link
|
234
|
+
app.say "Help guide: https://example.com/help"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
### Progressive Enhancement
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
def collect_feedback
|
243
|
+
rating = app.screen(:rating) do |prompt|
|
244
|
+
if whatsapp?
|
245
|
+
# Rich interactive buttons for WhatsApp
|
246
|
+
prompt.select "Rate our service:", {
|
247
|
+
"5" => "⭐⭐⭐⭐⭐ Excellent",
|
248
|
+
"4" => "⭐⭐⭐⭐ Good",
|
249
|
+
"3" => "⭐⭐⭐ Average",
|
250
|
+
"2" => "⭐⭐ Poor",
|
251
|
+
"1" => "⭐ Very Poor"
|
252
|
+
}
|
253
|
+
else
|
254
|
+
# Simple numbered list for USSD
|
255
|
+
prompt.select "Rate our service (1-5):", ["1", "2", "3", "4", "5"]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
app.say "Thank you for rating us #{rating} stars!"
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
def whatsapp?
|
265
|
+
app.context["request.gateway"] == :whatsapp_cloud_api
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
## Best Practices
|
270
|
+
|
271
|
+
### Keep Methods Focused
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# ✅ Good: Single responsibility
|
275
|
+
def collect_contact_info
|
276
|
+
name = app.screen(:name) { |p| p.ask "Name:" }
|
277
|
+
email = app.screen(:email) { |p| p.ask "Email:" }
|
278
|
+
{ name: name, email: email }
|
279
|
+
end
|
280
|
+
|
281
|
+
# ❌ Avoid: Too much in one method
|
282
|
+
def handle_everything
|
283
|
+
# 50+ lines of mixed logic
|
284
|
+
end
|
285
|
+
```
|
286
|
+
|
287
|
+
### Use Meaningful Screen Names
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# ✅ Good: Descriptive names
|
291
|
+
app.screen(:billing_address) { |p| p.ask "Billing address:" }
|
292
|
+
app.screen(:confirm_payment) { |p| p.yes? "Confirm $#{amount}?" }
|
293
|
+
|
294
|
+
# ❌ Avoid: Generic names
|
295
|
+
app.screen(:input1) { |p| p.ask "Address:" }
|
296
|
+
app.screen(:confirm) { |p| p.yes? "OK?" }
|
297
|
+
```
|
298
|
+
|
299
|
+
### Handle Edge Cases
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
def show_order_history
|
303
|
+
orders = fetch_user_orders
|
304
|
+
|
305
|
+
if orders.empty?
|
306
|
+
app.say "You have no previous orders."
|
307
|
+
return
|
308
|
+
end
|
309
|
+
|
310
|
+
choice = app.screen(:order) do |prompt|
|
311
|
+
prompt.select "Select order:", orders.map(&:display_name)
|
312
|
+
end
|
313
|
+
|
314
|
+
show_order_details(orders.find { |o| o.display_name == choice })
|
315
|
+
end
|
316
|
+
```
|
317
|
+
|
318
|
+
## Testing Flows
|
319
|
+
|
320
|
+
See [Testing Guide](testing.md) for comprehensive testing strategies.
|
Binary file
|