rails_onboarding 0.6.1

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 (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. metadata +303 -0
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module RailsOnboarding
6
+ # CDN Support module for serving assets from a CDN
7
+ #
8
+ # This module provides helpers and configuration for serving
9
+ # onboarding assets (JavaScript, CSS, images) from a CDN
10
+ # to improve performance through better caching and geographic distribution
11
+ module CdnSupport
12
+ extend ActiveSupport::Concern
13
+
14
+ # Get the CDN host URL
15
+ #
16
+ # @return [String, nil] CDN host URL or nil if not configured
17
+ def self.cdn_host
18
+ @cdn_host ||= begin
19
+ # Check Rails configuration first
20
+ if defined?(Rails.application) && Rails.application.config.action_controller.asset_host
21
+ Rails.application.config.action_controller.asset_host
22
+ else
23
+ ENV["RAILS_ONBOARDING_CDN_HOST"]
24
+ end
25
+ end
26
+ end
27
+
28
+ # Set the CDN host URL
29
+ #
30
+ # @param host [String] CDN host URL
31
+ def self.cdn_host=(host)
32
+ @cdn_host = host
33
+ end
34
+
35
+ # Check if CDN is enabled
36
+ #
37
+ # @return [Boolean]
38
+ def self.cdn_enabled?
39
+ cdn_host.present? && !Rails.env.development?
40
+ end
41
+
42
+ # Get asset URL with CDN support
43
+ #
44
+ # @param asset_path [String] The asset path (e.g., 'rails_onboarding/application.css')
45
+ # @param asset_type [Symbol] Asset type (:stylesheet, :javascript, :image)
46
+ # @return [String] Full URL to the asset
47
+ def self.cdn_asset_url(asset_path, asset_type: :stylesheet)
48
+ if cdn_enabled?
49
+ case asset_type
50
+ when :stylesheet
51
+ "#{cdn_host}/assets/#{asset_path}"
52
+ when :javascript
53
+ "#{cdn_host}/assets/#{asset_path}"
54
+ when :image
55
+ "#{cdn_host}/assets/#{asset_path}"
56
+ else
57
+ "#{cdn_host}/assets/#{asset_path}"
58
+ end
59
+ else
60
+ # Fall back to local asset path
61
+ "/assets/#{asset_path}"
62
+ end
63
+ end
64
+
65
+ # Preload critical onboarding assets
66
+ #
67
+ # @return [Array<Hash>] Array of asset preload hints
68
+ def self.preload_assets
69
+ [
70
+ {
71
+ href: cdn_asset_url("rails_onboarding/application.css", asset_type: :stylesheet),
72
+ as: "style",
73
+ type: "text/css"
74
+ },
75
+ {
76
+ href: cdn_asset_url("rails_onboarding/application.js", asset_type: :javascript),
77
+ as: "script",
78
+ type: "text/javascript"
79
+ }
80
+ ]
81
+ end
82
+
83
+ # Generate cache-busting URL with versioning
84
+ #
85
+ # @param asset_path [String] The asset path
86
+ # @param version [String] Asset version (defaults to gem version)
87
+ # @param asset_type [Symbol] Asset type (:stylesheet, :javascript, :image)
88
+ # @return [String] URL with version parameter
89
+ def self.versioned_asset_url(asset_path, version: nil, asset_type: :stylesheet)
90
+ version ||= RailsOnboarding::VERSION
91
+ url = cdn_asset_url(asset_path, asset_type: asset_type)
92
+ "#{url}?v=#{version}"
93
+ end
94
+
95
+ # Make these methods available as class methods when included
96
+ class_methods do
97
+ delegate :cdn_host, :cdn_host=, :cdn_enabled?, :cdn_asset_url,
98
+ :preload_assets, :versioned_asset_url, to: CdnSupport
99
+ end
100
+
101
+ module_function
102
+
103
+ # Helper method for view templates
104
+ # Generates link tags with CDN support and preload hints
105
+ #
106
+ # @param asset_path [String] Asset path
107
+ # @param options [Hash] Additional options
108
+ # @return [String] HTML link tag
109
+ def stylesheet_link_tag_with_cdn(asset_path, **options)
110
+ url = CdnSupport.versioned_asset_url(asset_path, asset_type: :stylesheet)
111
+
112
+ preload = options.delete(:preload)
113
+ crossorigin = options.delete(:crossorigin)
114
+
115
+ tag_options = {
116
+ rel: "stylesheet",
117
+ href: url
118
+ }.merge(options)
119
+
120
+ tag_options[:crossorigin] = crossorigin if crossorigin
121
+
122
+ tags = [ "<link #{cdn_tag_attributes(tag_options)} />" ]
123
+
124
+ if preload
125
+ tags.unshift("<link #{cdn_tag_attributes(rel: 'preload', href: url, as: 'style')} />")
126
+ end
127
+
128
+ tags.join("\n").html_safe
129
+ end
130
+
131
+ # Helper method for JavaScript tags with CDN support
132
+ #
133
+ # @param asset_path [String] Asset path
134
+ # @param options [Hash] Additional options
135
+ # @return [String] HTML script tag
136
+ def javascript_include_tag_with_cdn(asset_path, **options)
137
+ url = CdnSupport.versioned_asset_url(asset_path, asset_type: :javascript)
138
+
139
+ preload = options.delete(:preload)
140
+ defer = options.delete(:defer) { true } # Default to defer
141
+ async = options.delete(:async)
142
+
143
+ tag_options = {
144
+ src: url,
145
+ type: "text/javascript"
146
+ }.merge(options)
147
+
148
+ tag_options[:defer] = "defer" if defer && !async
149
+ tag_options[:async] = "async" if async
150
+
151
+ tags = [ "<script #{cdn_tag_attributes(tag_options)}></script>" ]
152
+
153
+ if preload
154
+ tags.unshift("<link #{cdn_tag_attributes(rel: 'preload', href: url, as: 'script')} />")
155
+ end
156
+
157
+ tags.join("\n").html_safe
158
+ end
159
+
160
+ # Generate resource hints for DNS prefetch and preconnect
161
+ #
162
+ # @return [String] HTML meta tags for resource hints
163
+ def cdn_resource_hints
164
+ return "" unless CdnSupport.cdn_enabled?
165
+
166
+ cdn_host = ERB::Util.html_escape(CdnSupport.cdn_host)
167
+ <<~HTML.html_safe
168
+ <!-- DNS Prefetch and Preconnect for CDN -->
169
+ <link rel="dns-prefetch" href="#{cdn_host}" />
170
+ <link rel="preconnect" href="#{cdn_host}" crossorigin />
171
+ HTML
172
+ end
173
+
174
+ # Render tag attributes with every value HTML-escaped, so a CDN host or
175
+ # asset path containing a quote or angle bracket can't break out of the
176
+ # attribute it's placed in. Used in place of ActionView's tag helpers
177
+ # because these methods are also called as CdnSupport module functions,
178
+ # outside any view context where `tag`/`content_tag` would be available.
179
+ def cdn_tag_attributes(attributes)
180
+ attributes.map { |key, value| "#{key}=\"#{ERB::Util.html_escape(value)}\"" }.join(" ")
181
+ end
182
+
183
+ # Configure CDN headers for optimal caching
184
+ #
185
+ # @param max_age [Integer] Cache max age in seconds (default: 1 year)
186
+ # @return [Hash] Headers for CDN caching
187
+ def cdn_cache_headers(max_age: 31_536_000)
188
+ {
189
+ "Cache-Control" => "public, max-age=#{max_age}, immutable",
190
+ "Expires" => max_age.seconds.from_now.httpdate,
191
+ "Vary" => "Accept-Encoding"
192
+ }
193
+ end
194
+ end
195
+ end
196
+
197
+ # Extend ActionView helpers if in Rails context
198
+ if defined?(ActionView::Base)
199
+ ActionView::Base.class_eval do
200
+ include RailsOnboarding::CdnSupport
201
+ end
202
+ end
@@ -0,0 +1,24 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # A/B testing configuration.
4
+ module AbTesting
5
+ attr_accessor :enable_ab_testing, :ab_tests
6
+
7
+ # Get a specific A/B test configuration
8
+ #
9
+ # @param test_name [Symbol, String] The name of the test
10
+ # @return [Hash, nil] The test configuration or nil
11
+ def ab_test(test_name)
12
+ return nil unless enable_ab_testing
13
+ ab_tests[test_name.to_sym]
14
+ end
15
+
16
+ private
17
+
18
+ def initialize_ab_testing
19
+ @enable_ab_testing = false
20
+ @ab_tests = {}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Analytics tracking configuration.
4
+ module Analytics
5
+ attr_accessor :enable_analytics, :analytics_session_timeout_minutes
6
+ attr_reader :analytics_data_retention_days, :analytics_retention_days
7
+
8
+ # Keep analytics_retention_days in sync with analytics_data_retention_days
9
+ def analytics_retention_days=(value)
10
+ @analytics_retention_days = value
11
+ @analytics_data_retention_days = value
12
+ end
13
+
14
+ def analytics_data_retention_days=(value)
15
+ @analytics_data_retention_days = value
16
+ @analytics_retention_days = value
17
+ end
18
+
19
+ private
20
+
21
+ def initialize_analytics
22
+ @enable_analytics = true
23
+ @analytics_data_retention_days = 365 # Keep analytics data for 1 year
24
+ @analytics_retention_days = 365 # Alias for analytics_data_retention_days
25
+ @analytics_session_timeout_minutes = 30 # Consider session ended after 30 minutes of inactivity
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Integration & compatibility options (Devise, Turbo, API mode,
4
+ # background jobs, mailer).
5
+ module Integrations
6
+ attr_accessor :devise_integration_enabled,
7
+ :redirect_unconfirmed_to_onboarding,
8
+ :turbo_streams_enabled,
9
+ :turbo_morphing_enabled,
10
+ :api_mode_enabled,
11
+ :api_authentication_method,
12
+ :background_jobs_enabled,
13
+ :background_jobs_queue,
14
+ :mailer_from
15
+
16
+ private
17
+
18
+ def initialize_integrations
19
+ @devise_integration_enabled = true
20
+ @redirect_unconfirmed_to_onboarding = false
21
+ @turbo_streams_enabled = true
22
+ @turbo_morphing_enabled = false
23
+ @api_mode_enabled = false
24
+ @api_authentication_method = :token
25
+ @background_jobs_enabled = false
26
+ @background_jobs_queue = :default
27
+ @mailer_from = "noreply@example.com"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,92 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Milestone configuration, lookups, and trigger matching.
4
+ module Milestones
5
+ def enable_milestones
6
+ tenant_override(:enable_milestones, @enable_milestones)
7
+ end
8
+
9
+ attr_writer :enable_milestones
10
+
11
+ def milestones
12
+ tenant_override(:milestones, @milestones)
13
+ end
14
+
15
+ # Override setter to clear cache when configuration changes
16
+ def milestones=(value)
17
+ clear_cache!
18
+ @milestones = value
19
+ end
20
+
21
+ def milestone_by_key(key)
22
+ return nil if key.nil?
23
+
24
+ @milestone_by_key_cache ||= {}
25
+ @milestone_by_key_cache[key.to_sym] ||= milestones.find { |m| m[:key].to_sym == key.to_sym }
26
+ end
27
+
28
+ def milestones_for_trigger(trigger, conditions = {})
29
+ cache_key = [ trigger, conditions ].hash
30
+ @milestones_for_trigger_cache ||= {}
31
+ @milestones_for_trigger_cache[cache_key] ||= milestones.select do |milestone|
32
+ # Match on trigger
33
+ next false unless milestone[:trigger] == trigger.to_sym
34
+
35
+ # If no conditions are provided, match all milestones with this trigger
36
+ next true if conditions.empty?
37
+
38
+ # If milestone has no conditions, but we're providing conditions, don't match
39
+ next false if milestone[:conditions].nil?
40
+
41
+ # Both have conditions, check if they match
42
+ conditions_match?(milestone[:conditions], conditions)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def conditions_match?(milestone_conditions, trigger_conditions)
49
+ return true if milestone_conditions.nil?
50
+
51
+ milestone_conditions.all? do |key, value|
52
+ trigger_conditions[key] == value || trigger_conditions[key.to_s] == value ||
53
+ trigger_conditions[key.to_sym] == value
54
+ end
55
+ end
56
+
57
+ def initialize_milestones
58
+ @enable_milestones = false
59
+
60
+ # Default milestones - can be customized
61
+ @milestones = [
62
+ {
63
+ key: :welcome_completed,
64
+ title: "Welcome Aboard!",
65
+ description: "You completed the welcome step",
66
+ icon: "🎉",
67
+ points: 10,
68
+ trigger: :onboarding_step_completed,
69
+ conditions: { step: :welcome }
70
+ },
71
+ {
72
+ key: :onboarding_completed,
73
+ title: "Onboarding Champion",
74
+ description: "You completed the entire onboarding flow",
75
+ icon: "🏆",
76
+ points: 50,
77
+ trigger: :onboarding_completed
78
+ },
79
+ {
80
+ key: :early_adopter,
81
+ title: "Early Adopter",
82
+ description: "You joined within the first hour",
83
+ icon: "⚡",
84
+ points: 100,
85
+ trigger: :custom,
86
+ conditions: { early_adopter: true }
87
+ }
88
+ ]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Per-user-type personalized onboarding flows.
4
+ module Personalization
5
+ attr_accessor :personalization_enabled, :user_type_method, :personalized_flows
6
+
7
+ # Get a personalized flow for a user type
8
+ #
9
+ # @param user_type [Symbol, String] The user type
10
+ # @return [Array, nil] The personalized steps or nil
11
+ def personalized_flow(user_type)
12
+ return nil unless personalization_enabled
13
+ personalized_flows[user_type.to_sym]
14
+ end
15
+
16
+ private
17
+
18
+ def initialize_personalization
19
+ @personalization_enabled = false
20
+ @user_type_method = :user_type # Method to call on user to determine their type
21
+ @personalized_flows = {}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Progressive feature disclosure configuration.
4
+ module ProgressiveDisclosure
5
+ attr_accessor :progressive_disclosure_enabled, :progressive_features
6
+
7
+ # Check if a feature should be shown based on progressive disclosure
8
+ #
9
+ # @param feature_key [Symbol, String] The feature key
10
+ # @param user [Object] The user object
11
+ # @return [Boolean] True if feature should be shown
12
+ def show_progressive_feature?(feature_key, user)
13
+ return true unless progressive_disclosure_enabled
14
+
15
+ feature = progressive_features.find { |f| f[:key] == feature_key.to_sym }
16
+ return true unless feature
17
+
18
+ # Check if feature meets its reveal conditions
19
+ case feature[:reveal_condition]
20
+ when :time_based
21
+ user.created_at + feature[:delay].seconds <= Time.current
22
+ when :action_based
23
+ user.send(feature[:check_method]) if user.respond_to?(feature[:check_method])
24
+ when :step_based
25
+ step_index(user.onboarding_current_step) >= step_index(feature[:after_step])
26
+ else
27
+ true
28
+ end
29
+ rescue StandardError
30
+ true # Default to showing feature if check fails
31
+ end
32
+
33
+ private
34
+
35
+ def initialize_progressive_disclosure
36
+ @progressive_disclosure_enabled = false
37
+ @progressive_features = []
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Rate limiting configuration (RailsOnboarding::RateLimitable) - opt-in.
4
+ module RateLimiting
5
+ attr_accessor :rate_limiting_enabled, :rate_limit_per_period, :rate_limit_period
6
+
7
+ private
8
+
9
+ def initialize_rate_limiting
10
+ # Defaulting this on would start enforcing a request limit on every
11
+ # host app that upgrades without them ever having asked for it.
12
+ @rate_limiting_enabled = false
13
+ @rate_limit_per_period = 60 # Number of requests allowed per period
14
+ @rate_limit_period = 60 # Period in seconds (60 seconds = 1 minute)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,121 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Onboarding step configuration and lookups.
4
+ module Steps
5
+ attr_accessor :onboarding_required_for
6
+
7
+ # Reads the active RailsOnboarding::Flow from the admin Flow Editor when one
8
+ # exists, falling back to the statically-configured steps otherwise. This
9
+ # keeps "activate a flow" a real, shared, durable change instead of a
10
+ # per-process global mutation - every process re-checks the database
11
+ # instead of caching a value that could go stale the moment another
12
+ # process (or another admin) activates a different flow.
13
+ def steps
14
+ flow_steps = active_flow_steps
15
+ return hydrate_code_options(flow_steps) if flow_steps
16
+
17
+ tenant_override(:steps, @steps)
18
+ end
19
+
20
+ # Override setter to clear cache when configuration changes
21
+ def steps=(value)
22
+ clear_cache!
23
+ @steps = value
24
+ end
25
+
26
+ def total_steps
27
+ @total_steps ||= steps.size
28
+ end
29
+
30
+ def step_by_name(name)
31
+ return nil if name.nil?
32
+
33
+ @step_by_name_cache ||= {}
34
+ @step_by_name_cache[name.to_sym] ||= steps.find do |s|
35
+ next false unless s.is_a?(Hash) && s[:name]
36
+ s[:name].to_sym == name.to_sym
37
+ end
38
+ end
39
+
40
+ def step_index(name)
41
+ return nil if name.nil?
42
+
43
+ @step_index_cache ||= {}
44
+ @step_index_cache[name.to_sym] ||= steps.find_index do |s|
45
+ next false unless s.is_a?(Hash) && s[:name]
46
+ s[:name].to_sym == name.to_sym
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def active_flow_steps
53
+ return nil unless defined?(RailsOnboarding::Flow)
54
+ return nil unless RailsOnboarding::Flow.table_exists?
55
+
56
+ RailsOnboarding::Flow.active.first&.steps
57
+ rescue StandardError
58
+ nil
59
+ end
60
+
61
+ # A flow persisted by the admin Flow Editor is stored as JSON, so any
62
+ # Proc-valued step option (:path, :complete_if, or a custom callable) was
63
+ # silently dropped on write - JSON can't serialize a Proc. Merge those
64
+ # code-only options back in from the statically-configured step of the
65
+ # same name, so activating a flow never disables a path-based step or its
66
+ # completion criteria. Presentation and ordering still come entirely from
67
+ # the flow; only behavior that can only live in code is re-hydrated.
68
+ def hydrate_code_options(flow_steps)
69
+ config_by_name = Array(@steps).each_with_object({}) do |step, index|
70
+ index[step[:name].to_sym] = step if step.is_a?(Hash) && step[:name]
71
+ end
72
+ return Array(flow_steps) if config_by_name.empty?
73
+
74
+ Array(flow_steps).map do |flow_step|
75
+ name = flow_step[:name] if flow_step.respond_to?(:[])
76
+ config_step = name && config_by_name[name.to_sym]
77
+ next flow_step unless config_step
78
+
79
+ code_options = config_step.select { |_key, value| value.is_a?(Proc) }
80
+ next flow_step if code_options.empty?
81
+
82
+ hydrated = flow_step.dup
83
+ code_options.each { |key, value| hydrated[key] = value }
84
+ hydrated
85
+ end
86
+ end
87
+
88
+ def initialize_steps
89
+ @onboarding_required_for = :new_users # :new_users, :all_users, or a Proc
90
+
91
+ # Default steps - can be customized
92
+ @steps = [
93
+ {
94
+ name: :welcome,
95
+ title: "Welcome",
96
+ icon: "🎉",
97
+ skippable: true
98
+ },
99
+ {
100
+ name: :profile,
101
+ title: "Setup Profile",
102
+ icon: "👤",
103
+ skippable: false
104
+ },
105
+ {
106
+ name: :first_action,
107
+ title: "First Action",
108
+ icon: "🚀",
109
+ skippable: false
110
+ },
111
+ {
112
+ name: :explore,
113
+ title: "Explore Features",
114
+ icon: "🔍",
115
+ skippable: true
116
+ }
117
+ ]
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,85 @@
1
+ module RailsOnboarding
2
+ class Configuration
3
+ # Pre-built onboarding step templates for common app types.
4
+ module Templates
5
+ attr_accessor :onboarding_templates
6
+
7
+ # Get an onboarding template by key
8
+ #
9
+ # @param template_key [Symbol, String] The template key
10
+ # @return [Hash, nil] The template configuration or nil
11
+ def template(template_key)
12
+ onboarding_templates[template_key.to_sym]
13
+ end
14
+
15
+ # Apply a template to the current configuration
16
+ #
17
+ # @param template_key [Symbol, String] The template key to apply
18
+ # @return [Boolean] True if template was applied successfully
19
+ def apply_template(template_key)
20
+ template = onboarding_templates[template_key.to_sym]
21
+ return false unless template
22
+
23
+ @steps = template[:steps]
24
+ true
25
+ end
26
+
27
+ private
28
+
29
+ def initialize_templates
30
+ @onboarding_templates = {
31
+ saas: {
32
+ name: "SaaS Application",
33
+ steps: [
34
+ { name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
35
+ { name: :account_setup, title: "Account Setup", icon: "👤", skippable: false },
36
+ { name: :team_invite, title: "Invite Team", icon: "👥", skippable: true },
37
+ { name: :first_project, title: "Create Project", icon: "📁", skippable: false },
38
+ { name: :integration, title: "Connect Tools", icon: "🔌", skippable: true }
39
+ ]
40
+ },
41
+ ecommerce: {
42
+ name: "E-commerce Platform",
43
+ steps: [
44
+ { name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
45
+ { name: :store_setup, title: "Setup Store", icon: "🏪", skippable: false },
46
+ { name: :first_product, title: "Add Product", icon: "📦", skippable: false },
47
+ { name: :payment_setup, title: "Payment Setup", icon: "💳", skippable: false },
48
+ { name: :launch, title: "Launch Store", icon: "🚀", skippable: false }
49
+ ]
50
+ },
51
+ marketplace: {
52
+ name: "Marketplace",
53
+ steps: [
54
+ { name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
55
+ { name: :profile_setup, title: "Create Profile", icon: "👤", skippable: false },
56
+ { name: :verification, title: "Verify Account", icon: "✅", skippable: false },
57
+ { name: :first_listing, title: "Create Listing", icon: "📝", skippable: false },
58
+ { name: :explore, title: "Explore", icon: "🔍", skippable: true }
59
+ ]
60
+ },
61
+ community: {
62
+ name: "Community Platform",
63
+ steps: [
64
+ { name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
65
+ { name: :profile, title: "Setup Profile", icon: "👤", skippable: false },
66
+ { name: :interests, title: "Choose Interests", icon: "❤️", skippable: true },
67
+ { name: :first_post, title: "Create Post", icon: "✍️", skippable: false },
68
+ { name: :connect, title: "Connect", icon: "🤝", skippable: true }
69
+ ]
70
+ },
71
+ education: {
72
+ name: "Educational Platform",
73
+ steps: [
74
+ { name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
75
+ { name: :student_setup, title: "Student Info", icon: "🎓", skippable: false },
76
+ { name: :course_selection, title: "Choose Courses", icon: "📚", skippable: false },
77
+ { name: :first_lesson, title: "First Lesson", icon: "📖", skippable: false },
78
+ { name: :study_plan, title: "Study Plan", icon: "📅", skippable: true }
79
+ ]
80
+ }
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end