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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +84 -1229
  5. data/docs/configuration.md +337 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/testing.md +475 -0
  11. data/docs/ussd-setup.md +306 -0
  12. data/docs/whatsapp-setup.md +162 -0
  13. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  14. data/examples/simulator_controller.rb +9 -18
  15. data/examples/ussd_controller.rb +32 -38
  16. data/examples/whatsapp_controller.rb +32 -125
  17. data/examples/whatsapp_media_examples.rb +68 -336
  18. data/examples/whatsapp_message_job.rb +5 -3
  19. data/flow_chat.gemspec +6 -2
  20. data/lib/flow_chat/base_processor.rb +48 -2
  21. data/lib/flow_chat/config.rb +5 -0
  22. data/lib/flow_chat/context.rb +13 -1
  23. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  24. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  25. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  26. data/lib/flow_chat/instrumentation.rb +70 -0
  27. data/lib/flow_chat/prompt.rb +20 -20
  28. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  29. data/lib/flow_chat/session/middleware.rb +37 -4
  30. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  31. data/lib/flow_chat/simulator/controller.rb +6 -6
  32. data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
  33. data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
  34. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  35. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  36. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  37. data/lib/flow_chat/ussd/processor.rb +14 -0
  38. data/lib/flow_chat/ussd/renderer.rb +1 -1
  39. data/lib/flow_chat/version.rb +1 -1
  40. data/lib/flow_chat/whatsapp/client.rb +99 -12
  41. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  42. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +120 -34
  43. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  44. data/lib/flow_chat/whatsapp/processor.rb +8 -0
  45. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  46. data/lib/flow_chat.rb +23 -0
  47. metadata +22 -11
  48. data/.travis.yml +0 -6
  49. data/app/controllers/demo_controller.rb +0 -101
  50. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  51. data/config/routes_demo.rb +0 -59
  52. data/examples/initializer.rb +0 -86
  53. data/examples/media_prompts_examples.rb +0 -27
  54. data/images/ussd_simulator.png +0 -0
@@ -0,0 +1,337 @@
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
+ ## USSD Configuration
23
+
24
+ ```ruby
25
+ # USSD pagination settings
26
+ FlowChat::Config.ussd.pagination_page_size = 140 # characters per page
27
+ FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next page
28
+ FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
29
+ FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
30
+ FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
31
+
32
+ # Resumable sessions
33
+ FlowChat::Config.ussd.resumable_sessions_enabled = true # default
34
+ FlowChat::Config.ussd.resumable_sessions_timeout_seconds = 300 # 5 minutes
35
+ ```
36
+
37
+ ## WhatsApp Configuration
38
+
39
+ ```ruby
40
+ # Message handling modes
41
+ FlowChat::Config.whatsapp.message_handling_mode = :inline # :inline, :background, :simulator
42
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
43
+ ```
44
+
45
+ ### WhatsApp Credential Configuration
46
+
47
+ #### Option 1: Rails Credentials
48
+
49
+ ```bash
50
+ rails credentials:edit
51
+ ```
52
+
53
+ ```yaml
54
+ whatsapp:
55
+ access_token: "your_access_token"
56
+ phone_number_id: "your_phone_number_id"
57
+ verify_token: "your_verify_token"
58
+ app_id: "your_app_id"
59
+ app_secret: "your_app_secret"
60
+ business_account_id: "your_business_account_id"
61
+ skip_signature_validation: false
62
+ ```
63
+
64
+ #### Option 2: Environment Variables
65
+
66
+ ```bash
67
+ export WHATSAPP_ACCESS_TOKEN="your_access_token"
68
+ export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"
69
+ export WHATSAPP_VERIFY_TOKEN="your_verify_token"
70
+ export WHATSAPP_APP_ID="your_app_id"
71
+ export WHATSAPP_APP_SECRET="your_app_secret"
72
+ export WHATSAPP_BUSINESS_ACCOUNT_ID="your_business_account_id"
73
+ export WHATSAPP_SKIP_SIGNATURE_VALIDATION="false"
74
+ ```
75
+
76
+ #### Option 3: Programmatic Configuration
77
+
78
+ ```ruby
79
+ config = FlowChat::Whatsapp::Configuration.new(:my_config) # Named configuration
80
+ config.access_token = "your_access_token"
81
+ config.phone_number_id = "your_phone_number_id"
82
+ config.verify_token = "your_verify_token"
83
+ config.app_id = "your_app_id"
84
+ config.app_secret = "your_app_secret"
85
+ config.business_account_id = "your_business_account_id"
86
+ config.skip_signature_validation = false
87
+ # Configuration is automatically registered as :my_config
88
+ ```
89
+
90
+ **⚠️ Important for Background Jobs:** When using background mode with programmatic configurations, you must register them in an initializer:
91
+
92
+ ```ruby
93
+ # config/initializers/whatsapp_configs.rb
94
+ # Register configurations so background jobs can access them
95
+ production_config = FlowChat::Whatsapp::Configuration.new(:production)
96
+ production_config.access_token = ENV['PROD_WHATSAPP_TOKEN']
97
+ # ... other settings
98
+
99
+ staging_config = FlowChat::Whatsapp::Configuration.new(:staging)
100
+ staging_config.access_token = ENV['STAGING_WHATSAPP_TOKEN']
101
+ # ... other settings
102
+ ```
103
+
104
+ Then use named configurations in controllers:
105
+
106
+ ```ruby
107
+ # Use registered configuration
108
+ config = FlowChat::Whatsapp::Configuration.get(:production)
109
+ processor = FlowChat::Whatsapp::Processor.new(self) do |config|
110
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, config
111
+ end
112
+ ```
113
+
114
+ ## Security Configuration
115
+
116
+ ### WhatsApp Security
117
+
118
+ ```ruby
119
+ # Production security (recommended)
120
+ config.app_secret = "your_whatsapp_app_secret"
121
+ config.skip_signature_validation = false # default
122
+
123
+ # Development mode (disable validation)
124
+ config.app_secret = nil
125
+ config.skip_signature_validation = true
126
+ ```
127
+
128
+ ### Simulator Security
129
+
130
+ ```ruby
131
+ # Use Rails secret for uniqueness
132
+ FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_simulator"
133
+
134
+ # Or use dedicated secret
135
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
136
+ ```
137
+
138
+ ## Environment-Specific Configuration
139
+
140
+ ```ruby
141
+ # config/initializers/flowchat.rb
142
+ case Rails.env
143
+ when 'development'
144
+ FlowChat::Config.whatsapp.message_handling_mode = :simulator
145
+ FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
146
+
147
+ when 'test'
148
+ FlowChat::Config.whatsapp.message_handling_mode = :simulator
149
+ FlowChat::Config.simulator_secret = "test_secret"
150
+
151
+ when 'staging'
152
+ FlowChat::Config.whatsapp.message_handling_mode = :inline
153
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
154
+
155
+ when 'production'
156
+ FlowChat::Config.whatsapp.message_handling_mode = :background
157
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
158
+ FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
159
+ end
160
+ ```
161
+
162
+ ## Processor Configuration
163
+
164
+ ### USSD Processor
165
+
166
+ ```ruby
167
+ processor = FlowChat::Ussd::Processor.new(self) do |config|
168
+ # Gateway (required)
169
+ config.use_gateway FlowChat::Ussd::Gateway::Nalo
170
+
171
+ # Session storage (required)
172
+ config.use_session_store FlowChat::Session::CacheSessionStore
173
+
174
+ # Optional middleware
175
+ config.use_middleware MyCustomMiddleware
176
+
177
+ # Optional resumable sessions
178
+ config.use_resumable_sessions
179
+ end
180
+ ```
181
+
182
+ ### WhatsApp Processor
183
+
184
+ ```ruby
185
+ processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: Rails.env.development?) do |config|
186
+ # Gateway (required)
187
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
188
+
189
+ # Session storage (required)
190
+ config.use_session_store FlowChat::Session::CacheSessionStore
191
+
192
+ # Optional custom configuration
193
+ config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, custom_whatsapp_config
194
+ end
195
+ ```
196
+
197
+ ## Session Store Options
198
+
199
+ ### Cache Session Store
200
+
201
+ ```ruby
202
+ config.use_session_store FlowChat::Session::CacheSessionStore
203
+ ```
204
+
205
+ Uses Rails cache backend with automatic TTL management. This is the primary session store available in FlowChat.
206
+
207
+ ## Middleware Configuration
208
+
209
+ ### Built-in Middleware
210
+
211
+ ```ruby
212
+ # Pagination (USSD only, automatic)
213
+ FlowChat::Ussd::Middleware::Pagination
214
+
215
+ # Session management (automatic)
216
+ FlowChat::Session::Middleware
217
+
218
+ # Gateway communication (automatic)
219
+ FlowChat::Ussd::Gateway::Nalo
220
+ FlowChat::Whatsapp::Gateway::CloudApi
221
+ ```
222
+
223
+ ### Custom Middleware
224
+
225
+ ```ruby
226
+ class LoggingMiddleware
227
+ def initialize(app)
228
+ @app = app
229
+ end
230
+
231
+ def call(context)
232
+ Rails.logger.info "Processing request: #{context.input}"
233
+ result = @app.call(context)
234
+ Rails.logger.info "Response: #{result[1]}"
235
+ result
236
+ end
237
+ end
238
+
239
+ # Use custom middleware
240
+ config.use_middleware LoggingMiddleware
241
+ ```
242
+
243
+ ## Validation Configuration
244
+
245
+ ### Error Display Options
246
+
247
+ ```ruby
248
+ # Combine validation error with original message (default)
249
+ FlowChat::Config.combine_validation_error_with_message = true
250
+ # User sees: "Invalid email format\n\nEnter your email:"
251
+
252
+ # Show only validation error
253
+ FlowChat::Config.combine_validation_error_with_message = false
254
+ # User sees: "Invalid email format"
255
+ ```
256
+
257
+ ## Background Job Configuration
258
+
259
+ ### Job Class Setup
260
+
261
+ ```ruby
262
+ # app/jobs/whatsapp_message_job.rb
263
+ class WhatsappMessageJob < ApplicationJob
264
+ include FlowChat::Whatsapp::SendJobSupport
265
+
266
+ def perform(send_data)
267
+ perform_whatsapp_send(send_data)
268
+ end
269
+ end
270
+ ```
271
+
272
+ **Configuration Resolution:** The job automatically resolves configurations using:
273
+ 1. Named configuration from `send_data[:configuration_name]` if present
274
+ 2. Default configuration from credentials/environment variables
275
+
276
+ For custom resolution logic, override the configuration resolution:
277
+
278
+ ```ruby
279
+ class CustomWhatsappMessageJob < ApplicationJob
280
+ include FlowChat::Whatsapp::SendJobSupport
281
+
282
+ def perform(send_data)
283
+ perform_whatsapp_send(send_data)
284
+ end
285
+
286
+ private
287
+
288
+ def resolve_whatsapp_configuration(send_data)
289
+ # Custom logic to resolve configuration
290
+ tenant_id = ...
291
+ FlowChat::Whatsapp::Configuration.get("tenant_#{tenant_id}")
292
+ end
293
+ end
294
+ ```
295
+
296
+ ### Queue Configuration
297
+
298
+ ```ruby
299
+ # config/application.rb
300
+ config.active_job.queue_adapter = :sidekiq
301
+
302
+ # config/initializers/flowchat.rb
303
+ FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
304
+ ```
305
+
306
+ ## Instrumentation Configuration
307
+
308
+ ### Basic Setup
309
+
310
+ ```ruby
311
+ # Enable instrumentation
312
+ FlowChat.setup_instrumentation!
313
+ ```
314
+
315
+ ### Custom Event Subscribers
316
+
317
+ ```ruby
318
+ # Subscribe to specific events
319
+ ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |event|
320
+ # Custom handling
321
+ ExternalMonitoring.track_flow_execution(
322
+ event.payload[:flow_name],
323
+ event.duration
324
+ )
325
+ end
326
+
327
+ # Subscribe to all FlowChat events
328
+ ActiveSupport::Notifications.subscribe(/\.flow_chat$/) do |name, start, finish, id, payload|
329
+ CustomLogger.log_event(name, payload.merge(duration: finish - start))
330
+ end
331
+ ```
332
+
333
+ ## Configuration Validation
334
+
335
+ FlowChat validates configuration at runtime and provides helpful error messages:
336
+
337
+ 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