flow_chat 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/pages.yml +43 -0
  5. data/.github/workflows/release.yml +56 -0
  6. data/.standard.yml +4 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CLAUDE.md +327 -0
  9. data/CONTRIBUTING.md +134 -0
  10. data/Gemfile +1 -0
  11. data/README.md +189 -133
  12. data/Rakefile +17 -2
  13. data/SECURITY.md +42 -349
  14. data/docs/architecture.md +83 -0
  15. data/docs/async-background-processing.md +64 -0
  16. data/docs/configuration.md +110 -287
  17. data/docs/factory-pattern.md +58 -0
  18. data/docs/gateway-context-variables.md +168 -0
  19. data/docs/gateway-development.md +159 -0
  20. data/docs/getting-started.md +90 -0
  21. data/docs/instrumentation.md +95 -175
  22. data/docs/platforms/instagram.md +278 -0
  23. data/docs/platforms/messenger.md +205 -0
  24. data/docs/platforms/telegram.md +109 -0
  25. data/docs/platforms/ussd.md +78 -0
  26. data/docs/platforms/whatsapp.md +147 -0
  27. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  28. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  29. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  30. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  31. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  32. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  33. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  34. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  35. data/docs/testing.md +33 -426
  36. data/examples/custom_session_id_example.rb +119 -0
  37. data/examples/http_controller.rb +22 -20
  38. data/examples/intercom_configuration_example.rb +113 -0
  39. data/examples/intercom_controller.rb +182 -0
  40. data/examples/multi_tenant_whatsapp_controller.rb +63 -168
  41. data/examples/simulator_controller.rb +0 -1
  42. data/examples/ussd_controller.rb +88 -160
  43. data/examples/whatsapp_controller.rb +18 -17
  44. data/examples/whatsapp_media_examples.rb +27 -79
  45. data/flow_chat.gemspec +4 -0
  46. data/lib/flow_chat/app.rb +211 -0
  47. data/lib/flow_chat/async_job.rb +176 -0
  48. data/lib/flow_chat/choice_titles.rb +95 -0
  49. data/lib/flow_chat/config.rb +126 -23
  50. data/lib/flow_chat/delivery_error.rb +9 -0
  51. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  52. data/lib/flow_chat/factory.rb +94 -0
  53. data/lib/flow_chat/gateway_async_support.rb +106 -0
  54. data/lib/flow_chat/generic_async_job.rb +30 -0
  55. data/lib/flow_chat/http/configuration_error.rb +9 -0
  56. data/lib/flow_chat/http/gateway/simple.rb +104 -36
  57. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  58. data/lib/flow_chat/http/renderer.rb +3 -3
  59. data/lib/flow_chat/input.rb +86 -0
  60. data/lib/flow_chat/instagram/client.rb +32 -0
  61. data/lib/flow_chat/instagram/configuration.rb +147 -0
  62. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  63. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  64. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  65. data/lib/flow_chat/instagram/renderer.rb +23 -0
  66. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  67. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  68. data/lib/flow_chat/instrumentation.rb +182 -0
  69. data/lib/flow_chat/intercom/client.rb +161 -0
  70. data/lib/flow_chat/intercom/configuration.rb +102 -0
  71. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  72. data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
  73. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  74. data/lib/flow_chat/intercom/renderer.rb +123 -0
  75. data/lib/flow_chat/media.rb +121 -0
  76. data/lib/flow_chat/messenger/client.rb +264 -0
  77. data/lib/flow_chat/messenger/configuration.rb +103 -0
  78. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  79. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  80. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  81. data/lib/flow_chat/messenger/renderer.rb +150 -0
  82. data/lib/flow_chat/meta/challenge.rb +24 -0
  83. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  84. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  85. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  86. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  87. data/lib/flow_chat/meta/signature.rb +30 -0
  88. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  89. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  90. data/lib/flow_chat/named_configuration.rb +65 -0
  91. data/lib/flow_chat/phone_number_util.rb +37 -35
  92. data/lib/flow_chat/processor.rb +188 -0
  93. data/lib/flow_chat/prompt.rb +13 -16
  94. data/lib/flow_chat/renderers/markdown_support.rb +167 -0
  95. data/lib/flow_chat/security.rb +76 -0
  96. data/lib/flow_chat/session/middleware.rb +36 -11
  97. data/lib/flow_chat/simulator/controller.rb +31 -15
  98. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  99. data/lib/flow_chat/telegram/client.rb +283 -0
  100. data/lib/flow_chat/telegram/configuration.rb +78 -0
  101. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  102. data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
  103. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
  104. data/lib/flow_chat/telegram/renderer.rb +133 -0
  105. data/lib/flow_chat/telegram.rb +7 -0
  106. data/lib/flow_chat/text_truncator.rb +75 -0
  107. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  108. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  109. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  110. data/lib/flow_chat/ussd/renderer.rb +1 -1
  111. data/lib/flow_chat/version.rb +1 -1
  112. data/lib/flow_chat/whatsapp/client.rb +158 -20
  113. data/lib/flow_chat/whatsapp/configuration.rb +13 -52
  114. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  115. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
  116. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
  117. data/lib/flow_chat/whatsapp/renderer.rb +259 -64
  118. data/lib/flow_chat.rb +1 -1
  119. data/lib/tasks/release.rake +165 -0
  120. data/site/.nojekyll +0 -0
  121. data/site/.og-card.html +89 -0
  122. data/site/favicon.svg +6 -0
  123. data/site/index.html +209 -0
  124. data/site/og.png +0 -0
  125. metadata +132 -25
  126. data/docs/flows.md +0 -320
  127. data/docs/http-gateway-protocol.md +0 -432
  128. data/docs/images/simulator.png +0 -0
  129. data/docs/media.md +0 -153
  130. data/docs/sessions.md +0 -433
  131. data/docs/ussd-setup.md +0 -322
  132. data/docs/whatsapp-setup.md +0 -162
  133. data/examples/whatsapp_message_job.rb +0 -113
  134. data/lib/flow_chat/base_app.rb +0 -86
  135. data/lib/flow_chat/base_processor.rb +0 -146
  136. data/lib/flow_chat/http/app.rb +0 -6
  137. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  138. data/lib/flow_chat/http/processor.rb +0 -33
  139. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  140. data/lib/flow_chat/ussd/app.rb +0 -6
  141. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  142. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  143. data/lib/flow_chat/ussd/processor.rb +0 -39
  144. data/lib/flow_chat/whatsapp/app.rb +0 -29
  145. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  146. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  147. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
@@ -1,220 +1,115 @@
1
1
  # Example Multi-Tenant WhatsApp Controller
2
- # This shows how to configure different WhatsApp accounts per tenant/client
2
+ # Shows how to run different WhatsApp accounts per tenant on one endpoint.
3
+
4
+ # A single helper builds a Configuration from a set of environment variables,
5
+ # so each tenant is one line instead of a repeated block. Pass an anonymous
6
+ # configuration name (nil); Configuration#initialize requires a name argument.
7
+ module WhatsappConfigBuilder
8
+ def whatsapp_config_from_env(prefix)
9
+ FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
10
+ config.access_token = ENV["#{prefix}_WHATSAPP_ACCESS_TOKEN"]
11
+ config.phone_number_id = ENV["#{prefix}_WHATSAPP_PHONE_NUMBER_ID"]
12
+ config.verify_token = ENV["#{prefix}_WHATSAPP_VERIFY_TOKEN"]
13
+ config.app_secret = ENV["#{prefix}_WHATSAPP_APP_SECRET"]
14
+ end
15
+ end
16
+ end
3
17
 
4
- # Controller supporting multiple WhatsApp accounts per tenant
18
+ # Controller supporting multiple WhatsApp accounts per tenant.
5
19
  class MultiTenantWhatsappController < ApplicationController
20
+ include WhatsappConfigBuilder
21
+
6
22
  skip_forgery_protection
7
23
 
8
24
  def webhook
9
25
  tenant = determine_tenant(request)
10
- whatsapp_config = get_whatsapp_config_for_tenant(tenant)
26
+ whatsapp_config = config_for_tenant(tenant)
11
27
 
12
- processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
28
+ processor = FlowChat::Processor.new(self, enable_simulator: !Rails.env.production?) do |config|
13
29
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
14
30
  config.use_session_store FlowChat::Session::CacheSessionStore
15
31
  end
16
32
 
17
- flow_class = get_flow_for_tenant(tenant)
18
- processor.run flow_class, :main_page
33
+ processor.run flow_for_tenant(tenant), :main_page
19
34
  end
20
35
 
21
36
  private
22
37
 
23
38
  def determine_tenant(request)
24
- # Option 1: From subdomain
25
39
  return request.subdomain if request.subdomain.present?
26
40
 
27
- # Option 2: From path (e.g., /whatsapp/acme/webhook)
28
41
  tenant_from_path = request.path.match(%r{^/whatsapp/(\w+)/})&.captures&.first
29
42
  return tenant_from_path if tenant_from_path
30
43
 
31
- # Option 3: From header
32
- return request.headers["X-Tenant-ID"] if request.headers["X-Tenant-ID"]
33
-
34
- "default"
44
+ request.headers["X-Tenant-ID"].presence || "default"
35
45
  end
36
46
 
37
- def get_whatsapp_config_for_tenant(tenant)
47
+ def config_for_tenant(tenant)
38
48
  case tenant
39
- when "acme_corp"
40
- FlowChat::Whatsapp::Configuration.new.tap do |config|
41
- config.access_token = ENV["ACME_WHATSAPP_ACCESS_TOKEN"]
42
- config.phone_number_id = ENV["ACME_WHATSAPP_PHONE_NUMBER_ID"]
43
- config.verify_token = ENV["ACME_WHATSAPP_VERIFY_TOKEN"]
44
- config.app_secret = ENV["ACME_WHATSAPP_APP_SECRET"]
45
- end
46
-
47
- when "tech_startup"
48
- FlowChat::Whatsapp::Configuration.new.tap do |config|
49
- config.access_token = ENV["TECHSTARTUP_WHATSAPP_ACCESS_TOKEN"]
50
- config.phone_number_id = ENV["TECHSTARTUP_WHATSAPP_PHONE_NUMBER_ID"]
51
- config.verify_token = ENV["TECHSTARTUP_WHATSAPP_VERIFY_TOKEN"]
52
- config.app_secret = ENV["TECHSTARTUP_WHATSAPP_APP_SECRET"]
53
- end
54
-
55
- when "retail_store"
56
- # Load from database
57
- tenant_config = WhatsappConfiguration.find_by(tenant: tenant)
58
- FlowChat::Whatsapp::Configuration.new.tap do |config|
59
- config.access_token = tenant_config.access_token
60
- config.phone_number_id = tenant_config.phone_number_id
61
- config.verify_token = tenant_config.verify_token
62
- config.app_secret = tenant_config.app_secret
63
- end
64
-
65
- else
66
- FlowChat::Whatsapp::Configuration.from_credentials
49
+ when "acme_corp" then whatsapp_config_from_env("ACME")
50
+ when "tech_startup" then whatsapp_config_from_env("TECHSTARTUP")
51
+ when "retail_store" then config_from_database(tenant)
52
+ else FlowChat::Whatsapp::Configuration.from_credentials
67
53
  end
68
54
  end
69
55
 
70
- def get_flow_for_tenant(tenant)
71
- case tenant
72
- when "acme_corp"
73
- AcmeCorpFlow
74
- when "tech_startup"
75
- TechStartupFlow
76
- when "retail_store"
77
- RetailStoreFlow
78
- else
79
- WelcomeFlow
56
+ # Load a tenant's credentials from your own model.
57
+ def config_from_database(tenant)
58
+ record = WhatsappConfiguration.find_by!(tenant: tenant)
59
+ FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
60
+ config.access_token = record.access_token
61
+ config.phone_number_id = record.phone_number_id
62
+ config.verify_token = record.verify_token
63
+ config.app_secret = record.app_secret
80
64
  end
81
65
  end
66
+
67
+ def flow_for_tenant(tenant)
68
+ {
69
+ "acme_corp" => AcmeCorpFlow,
70
+ "tech_startup" => TechStartupFlow,
71
+ "retail_store" => RetailStoreFlow
72
+ }.fetch(tenant, WhatsappWelcomeFlow)
73
+ end
82
74
  end
83
75
 
84
- # Example: Dynamic Configuration from Database
76
+ # Resolve the account from the webhook's own phone_number_id, rather than the URL.
85
77
  class DatabaseWhatsappController < ApplicationController
78
+ include WhatsappConfigBuilder
79
+
86
80
  skip_forgery_protection
87
81
 
88
82
  def webhook
89
- business_account = find_business_account(params)
90
- return head :not_found if business_account.nil?
91
-
92
- whatsapp_config = FlowChat::Whatsapp::Configuration.new.tap do |config|
93
- config.access_token = business_account.whatsapp_access_token
94
- config.phone_number_id = business_account.whatsapp_phone_number_id
95
- config.verify_token = business_account.whatsapp_verify_token
96
- config.app_secret = business_account.whatsapp_app_secret
83
+ account = find_business_account(params)
84
+ return head :not_found if account.nil?
85
+
86
+ whatsapp_config = FlowChat::Whatsapp::Configuration.new(nil).tap do |config|
87
+ config.access_token = account.whatsapp_access_token
88
+ config.phone_number_id = account.whatsapp_phone_number_id
89
+ config.verify_token = account.whatsapp_verify_token
90
+ config.app_secret = account.whatsapp_app_secret
97
91
  end
98
92
 
99
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
93
+ processor = FlowChat::Processor.new(self) do |config|
100
94
  config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
101
95
  config.use_session_store FlowChat::Session::CacheSessionStore
102
96
  end
103
97
 
104
- processor.run business_account.flow_class.constantize, :main_page
98
+ processor.run account.flow_class.constantize, :main_page
105
99
  end
106
100
 
107
101
  private
108
102
 
109
103
  def find_business_account(params)
110
- # Find by phone number ID from webhook
111
- phone_number_id = extract_phone_number_id_from_webhook(params)
104
+ phone_number_id = params.dig(:entry, 0, :changes, 0, :value, :metadata, :phone_number_id)
112
105
  BusinessAccount.find_by(whatsapp_phone_number_id: phone_number_id)
113
106
  end
114
-
115
- def extract_phone_number_id_from_webhook(params)
116
- # Extract from webhook payload - implement based on your structure
117
- params.dig(:entry, 0, :changes, 0, :value, :metadata, :phone_number_id)
118
- end
119
- end
120
-
121
- # Example: Environment-based Configuration
122
- class EnvironmentWhatsappController < ApplicationController
123
- skip_forgery_protection
124
-
125
- def webhook
126
- # Different configurations for different environments
127
- whatsapp_config = case Rails.env
128
- when "production"
129
- production_whatsapp_config
130
- when "staging"
131
- staging_whatsapp_config
132
- when "development"
133
- development_whatsapp_config
134
- else
135
- FlowChat::Whatsapp::Configuration.from_credentials
136
- end
137
-
138
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
139
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, whatsapp_config
140
- config.use_session_store FlowChat::Session::CacheSessionStore
141
- end
142
-
143
- processor.run WelcomeFlow, :main_page
144
- end
145
-
146
- private
147
-
148
- def production_whatsapp_config
149
- FlowChat::Whatsapp::Configuration.new.tap do |config|
150
- config.access_token = ENV["PROD_WHATSAPP_ACCESS_TOKEN"]
151
- config.phone_number_id = ENV["PROD_WHATSAPP_PHONE_NUMBER_ID"]
152
- config.verify_token = ENV["PROD_WHATSAPP_VERIFY_TOKEN"]
153
- config.app_id = ENV["PROD_WHATSAPP_APP_ID"]
154
- config.app_secret = ENV["PROD_WHATSAPP_APP_SECRET"]
155
- config.business_account_id = ENV["PROD_WHATSAPP_BUSINESS_ACCOUNT_ID"]
156
- end
157
- end
158
-
159
- def staging_whatsapp_config
160
- FlowChat::Whatsapp::Configuration.new.tap do |config|
161
- config.access_token = ENV["STAGING_WHATSAPP_ACCESS_TOKEN"]
162
- config.phone_number_id = ENV["STAGING_WHATSAPP_PHONE_NUMBER_ID"]
163
- config.verify_token = ENV["STAGING_WHATSAPP_VERIFY_TOKEN"]
164
- config.app_id = ENV["STAGING_WHATSAPP_APP_ID"]
165
- config.app_secret = ENV["STAGING_WHATSAPP_APP_SECRET"]
166
- config.business_account_id = ENV["STAGING_WHATSAPP_BUSINESS_ACCOUNT_ID"]
167
- end
168
- end
169
-
170
- def development_whatsapp_config
171
- FlowChat::Whatsapp::Configuration.new.tap do |config|
172
- config.access_token = ENV["DEV_WHATSAPP_ACCESS_TOKEN"]
173
- config.phone_number_id = ENV["DEV_WHATSAPP_PHONE_NUMBER_ID"]
174
- config.verify_token = ENV["DEV_WHATSAPP_VERIFY_TOKEN"]
175
- config.app_id = ENV["DEV_WHATSAPP_APP_ID"]
176
- config.app_secret = ENV["DEV_WHATSAPP_APP_SECRET"]
177
- config.business_account_id = ENV["DEV_WHATSAPP_BUSINESS_ACCOUNT_ID"]
178
- end
179
- end
180
- end
181
-
182
- # Example: Simple Custom Configuration
183
- class CustomWhatsappController < ApplicationController
184
- skip_forgery_protection
185
-
186
- def webhook
187
- # Create custom configuration for this specific endpoint
188
- my_config = FlowChat::Whatsapp::Configuration.new
189
- my_config.access_token = "EAABs..." # Your specific access token
190
- my_config.phone_number_id = "123456789"
191
- my_config.verify_token = "my_verify_token"
192
- my_config.app_id = "your_app_id"
193
- my_config.app_secret = "your_app_secret"
194
- my_config.business_account_id = "your_business_account_id"
195
-
196
- processor = FlowChat::Whatsapp::Processor.new(self) do |config|
197
- config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi, my_config
198
- config.use_session_store FlowChat::Session::CacheSessionStore
199
- end
200
-
201
- processor.run CustomFlow, :main_page
202
- end
203
107
  end
204
108
 
205
- # Add routes for different tenants:
206
- # Rails.application.routes.draw do
207
- # # Subdomain-based routing
208
- # constraints subdomain: /\w+/ do
209
- # post '/whatsapp/webhook', to: 'multi_tenant_whatsapp#webhook'
109
+ # Add routes for the tenant endpoints, for example:
110
+ # Rails.application.routes.draw do
111
+ # constraints subdomain: /\w+/ do
112
+ # post "/whatsapp/webhook", to: "multi_tenant_whatsapp#webhook"
113
+ # end
114
+ # post "/whatsapp/:tenant/webhook", to: "multi_tenant_whatsapp#webhook"
210
115
  # end
211
- #
212
- # # Path-based routing
213
- # post '/whatsapp/:tenant/webhook', to: 'multi_tenant_whatsapp#webhook'
214
- #
215
- # # Environment-specific
216
- # post '/whatsapp/env/webhook', to: 'environment_whatsapp#webhook'
217
- #
218
- # # Custom endpoint
219
- # post '/whatsapp/custom/webhook', to: 'custom_whatsapp#webhook'
220
- # end
@@ -102,5 +102,4 @@ end
102
102
  # - Different API versions (v1, v2, etc.)
103
103
  # - Multi-tenant endpoints with different configurations
104
104
  # - Legacy endpoints alongside new ones
105
- # - External HTTP servers (run examples/http_simulator_test.rb)
106
105
  # - Different flow implementations for different endpoints
@@ -5,49 +5,83 @@ class UssdController < ApplicationController
5
5
  skip_forgery_protection
6
6
 
7
7
  def process_request
8
- processor = FlowChat::Ussd::Processor.new(self) do |config|
8
+ processor = FlowChat::Processor.new(self) do |config|
9
9
  config.use_gateway FlowChat::Ussd::Gateway::Nalo
10
- # Use Rails session for USSD (shorter sessions)
11
- config.use_session_store FlowChat::Session::RailsSessionStore
10
+ config.use_session_store FlowChat::Session::CacheSessionStore
11
+
12
+ # Durable sessions key on the phone number, so a conversation survives the
13
+ # telco rotating its session id on timeout. Optional.
14
+ config.use_durable_sessions
12
15
 
13
- # Enable durable sessions (optional)
14
- config.use_durable_sessions # Configures flow+platform isolation with durable sessions
16
+ # Other options you can set here:
17
+ #
18
+ # config.use_middleware LoggingMiddleware # custom middleware (see below)
19
+ # config.use_session_config( # explicit session boundaries
20
+ # boundaries: [:flow, :platform],
21
+ # hash_identifiers: true # hash phone numbers for privacy
22
+ # )
15
23
  end
16
24
 
17
- processor.run WelcomeFlow, :main_page
25
+ processor.run UssdWelcomeFlow, :main_page
26
+ end
27
+ end
28
+
29
+ # Add this route to your config/routes.rb:
30
+ # post "/ussd", to: "ussd#process_request"
31
+
32
+ # Pagination is configured globally, not per processor. USSD messages are
33
+ # length-limited, so long output is split into pages with navigation options:
34
+ #
35
+ # FlowChat::Config.ussd.pagination_page_size = 120
36
+ # FlowChat::Config.ussd.pagination_next_option = "#"
37
+ # FlowChat::Config.ussd.pagination_back_option = "0"
38
+
39
+ # A custom middleware, added above with config.use_middleware. It sees the
40
+ # normalized context on the way in and the [type, prompt, choices, media] result
41
+ # on the way out.
42
+ class LoggingMiddleware
43
+ def initialize(app)
44
+ @app = app
45
+ end
46
+
47
+ def call(context)
48
+ Rails.logger.info "USSD request from #{context["request.msisdn"]}: #{context.input}"
49
+ start_time = Time.current
50
+
51
+ result = @app.call(context)
52
+
53
+ duration = Time.current - start_time
54
+ Rails.logger.info "USSD response (#{duration.round(3)}s): #{result[1]}"
55
+
56
+ result
18
57
  end
19
58
  end
20
59
 
21
- # Example Flow for USSD
22
- # Add this to your Rails application as app/flow_chat/welcome_flow.rb
60
+ # Example flow for USSD
61
+ # Add this to your Rails application as app/flow_chat/ussd_welcome_flow.rb
23
62
 
24
- class WelcomeFlow < FlowChat::Flow
63
+ class UssdWelcomeFlow < FlowChat::Flow
25
64
  def main_page
26
- # Welcome the user
27
65
  name = app.screen(:name) do |prompt|
28
66
  prompt.ask "Welcome! What's your name?",
29
67
  transform: ->(input) { input.strip.titleize }
30
68
  end
31
69
 
32
- # Show main menu with numbered options (USSD style)
70
+ # Choices render as a numbered list; the flow works in the choice keys.
33
71
  choice = app.screen(:main_menu) do |prompt|
34
72
  prompt.select "Hi #{name}! Choose:", {
35
- "1" => "Account Info",
36
- "2" => "Make Payment",
37
- "3" => "Get Balance",
38
- "4" => "Support"
73
+ "info" => "Account Info",
74
+ "payment" => "Make Payment",
75
+ "balance" => "Get Balance",
76
+ "support" => "Support"
39
77
  }
40
78
  end
41
79
 
42
80
  case choice
43
- when "1"
44
- show_account_info
45
- when "2"
46
- make_payment
47
- when "3"
48
- get_balance
49
- when "4"
50
- customer_support
81
+ when "info" then show_account_info
82
+ when "payment" then make_payment
83
+ when "balance" then get_balance
84
+ when "support" then customer_support
51
85
  end
52
86
  end
53
87
 
@@ -56,21 +90,21 @@ class WelcomeFlow < FlowChat::Flow
56
90
  def show_account_info
57
91
  info_choice = app.screen(:account_info) do |prompt|
58
92
  prompt.select "Account Info:", {
59
- "1" => "Personal Details",
60
- "2" => "Balance",
61
- "3" => "Transaction History",
62
- "0" => "Main Menu"
93
+ "details" => "Personal Details",
94
+ "balance" => "Balance",
95
+ "history" => "Transaction History",
96
+ "back" => "Main Menu"
63
97
  }
64
98
  end
65
99
 
66
100
  case info_choice
67
- when "1"
68
- app.say "Name: John Doe\nPhone: #{app.phone_number}\nStatus: Active"
69
- when "2"
101
+ when "details"
102
+ app.say "Name: John Doe\nPhone: #{app.msisdn}\nStatus: Active"
103
+ when "balance"
70
104
  app.say "Balance: $150.75\nCredit: $1,000.00"
71
- when "3"
105
+ when "history"
72
106
  app.say "Recent:\n+$50.00 Deposit\n-$25.50 Purchase\n-$15.00 Transfer"
73
- when "0"
107
+ when "back"
74
108
  main_page
75
109
  end
76
110
  end
@@ -79,9 +113,9 @@ class WelcomeFlow < FlowChat::Flow
79
113
  amount = app.screen(:payment_amount) do |prompt|
80
114
  prompt.ask "Enter amount:",
81
115
  validate: ->(input) {
82
- amt = input.to_f
83
- return "Invalid amount" unless amt > 0
84
- return "Max $500" unless amt <= 500
116
+ amount = input.to_f
117
+ next "Invalid amount" unless amount > 0
118
+ next "Max $500" unless amount <= 500
85
119
  nil
86
120
  },
87
121
  transform: ->(input) { input.to_f }
@@ -89,19 +123,14 @@ class WelcomeFlow < FlowChat::Flow
89
123
 
90
124
  recipient = app.screen(:payment_recipient) do |prompt|
91
125
  prompt.ask "Recipient phone:",
92
- validate: ->(input) {
93
- return "10 digits required" unless input.match?(/\A\d{10}\z/)
94
- nil
95
- }
126
+ validate: ->(input) { "10 digits required" unless input.match?(/\A\d{10}\z/) }
96
127
  end
97
128
 
98
- # Confirmation screen
99
129
  confirmed = app.screen(:payment_confirmation) do |prompt|
100
130
  prompt.yes? "Pay $#{amount} to #{recipient}?"
101
131
  end
102
132
 
103
133
  if confirmed
104
- # Process payment (your business logic here)
105
134
  transaction_id = process_payment(amount, recipient)
106
135
  app.say "Payment successful!\nID: #{transaction_id}\nAmount: $#{amount}"
107
136
  else
@@ -110,26 +139,25 @@ class WelcomeFlow < FlowChat::Flow
110
139
  end
111
140
 
112
141
  def get_balance
113
- # Simulate balance check
114
- balance = check_account_balance(app.phone_number)
142
+ balance = check_account_balance(app.msisdn)
115
143
  app.say "Balance\n\nAvailable: $#{balance[:available]}\nPending: $#{balance[:pending]}"
116
144
  end
117
145
 
118
146
  def customer_support
119
147
  support_choice = app.screen(:support_menu) do |prompt|
120
148
  prompt.select "Support:", {
121
- "1" => "Report Issue",
122
- "2" => "Contact Info",
123
- "0" => "Main Menu"
149
+ "report" => "Report Issue",
150
+ "contact" => "Contact Info",
151
+ "back" => "Main Menu"
124
152
  }
125
153
  end
126
154
 
127
155
  case support_choice
128
- when "1"
156
+ when "report"
129
157
  report_issue
130
- when "2"
158
+ when "contact"
131
159
  app.say "Support:\nCall: 123-456-7890\nEmail: support@company.com\nHours: 9AM-5PM"
132
- when "0"
160
+ when "back"
133
161
  main_page
134
162
  end
135
163
  end
@@ -137,134 +165,34 @@ class WelcomeFlow < FlowChat::Flow
137
165
  def report_issue
138
166
  issue_type = app.screen(:issue_type) do |prompt|
139
167
  prompt.select "Issue type:", {
140
- "1" => "Payment Problem",
141
- "2" => "Account Access",
142
- "3" => "Service Error",
143
- "4" => "Other"
168
+ "payment" => "Payment Problem",
169
+ "access" => "Account Access",
170
+ "error" => "Service Error",
171
+ "other" => "Other"
144
172
  }
145
173
  end
146
174
 
147
175
  description = app.screen(:issue_description) do |prompt|
148
176
  prompt.ask "Describe issue:",
149
- validate: ->(input) {
150
- return "Min 10 characters" if input.length < 10
151
- nil
152
- }
177
+ validate: ->(input) { "Min 10 characters" if input.length < 10 }
153
178
  end
154
179
 
155
- # Save the issue (your business logic here)
156
- ticket_id = create_support_ticket(issue_type, description, app.phone_number)
157
-
180
+ ticket_id = create_support_ticket(issue_type, description, app.msisdn)
158
181
  app.say "Issue reported!\n\nTicket: #{ticket_id}\nWe'll contact you within 24hrs"
159
182
  end
160
183
 
161
- # Helper methods (implement your business logic)
184
+ # Replace these with your own business logic.
162
185
 
163
186
  def process_payment(amount, recipient)
164
- # Your payment processing logic here
165
- # Return transaction ID
166
187
  "TXN#{rand(100000..999999)}"
167
188
  end
168
189
 
169
- def check_account_balance(phone_number)
170
- # Your balance checking logic here
171
- {
172
- available: "150.75",
173
- pending: "25.00"
174
- }
190
+ def check_account_balance(msisdn)
191
+ {available: "150.75", pending: "25.00"}
175
192
  end
176
193
 
177
- def create_support_ticket(issue_type, description, phone_number)
178
- # Your ticket creation logic here
179
- Rails.logger.info "Ticket: #{issue_type} - #{description} from #{phone_number}"
194
+ def create_support_ticket(issue_type, description, msisdn)
195
+ Rails.logger.info "Ticket: #{issue_type} - #{description} from #{msisdn}"
180
196
  "TICKET#{rand(10000..99999)}"
181
197
  end
182
198
  end
183
-
184
- # Configuration Examples:
185
-
186
- # 1. Basic configuration with custom pagination
187
- # rubocop:disable Lint/DuplicateMethods
188
- class UssdController < ApplicationController
189
- skip_forgery_protection
190
-
191
- def process_request
192
- # Configure pagination for shorter messages
193
- FlowChat::Config.ussd.pagination_page_size = 120
194
- FlowChat::Config.ussd.pagination_next_option = "#"
195
- FlowChat::Config.ussd.pagination_back_option = "*"
196
-
197
- processor = FlowChat::Ussd::Processor.new(self) do |config|
198
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
199
- config.use_session_store FlowChat::Session::RailsSessionStore
200
- end
201
-
202
- processor.run WelcomeFlow, :main_page
203
- end
204
- end
205
- # rubocop:enable Lint/DuplicateMethods
206
-
207
- # 2. Configuration with custom middleware
208
- class LoggingMiddleware
209
- def initialize(app)
210
- @app = app
211
- end
212
-
213
- def call(context)
214
- Rails.logger.info "USSD Request from #{context["request.msisdn"]}: #{context.input}"
215
- start_time = Time.current
216
-
217
- result = @app.call(context)
218
-
219
- duration = Time.current - start_time
220
- Rails.logger.info "USSD Response (#{duration.round(3)}s): #{result[1]}"
221
-
222
- result
223
- end
224
- end
225
-
226
- # rubocop:disable Lint/DuplicateMethods
227
- class UssdController < ApplicationController
228
- skip_forgery_protection
229
-
230
- def process_request
231
- processor = FlowChat::Ussd::Processor.new(self) do |config|
232
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
233
- config.use_session_store FlowChat::Session::RailsSessionStore
234
- config.use_middleware LoggingMiddleware # Add custom logging
235
- config.use_durable_sessions # Enable durable sessions
236
-
237
- # Or configure session boundaries explicitly:
238
- # config.use_session_config(
239
- # boundaries: [:flow, :platform], # which boundaries to enforce
240
- # hash_identifiers: true # hash phone numbers for privacy
241
- # )
242
- end
243
-
244
- processor.run WelcomeFlow, :main_page
245
- end
246
- end
247
- # rubocop:enable Lint/DuplicateMethods
248
-
249
- # 3. Configuration with cache-based sessions for longer persistence
250
- # rubocop:disable Lint/DuplicateMethods
251
- class UssdController < ApplicationController
252
- skip_forgery_protection
253
-
254
- def process_request
255
- processor = FlowChat::Ussd::Processor.new(self) do |config|
256
- config.use_gateway FlowChat::Ussd::Gateway::Nalo
257
- # Use cache store for longer session persistence
258
- config.use_session_store FlowChat::Session::CacheSessionStore
259
- end
260
-
261
- processor.run WelcomeFlow, :main_page
262
- end
263
- end
264
- # rubocop:enable Lint/DuplicateMethods
265
-
266
- # Add this route to your config/routes.rb:
267
- # post '/ussd', to: 'ussd#process_request'
268
-
269
- # For Nsano gateway, use:
270
- # config.use_gateway FlowChat::Ussd::Gateway::Nsano