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,187 @@
1
+ module RailsOnboarding
2
+ # Session Manager
3
+ # Handles browser refresh/navigation during onboarding
4
+ # Persists onboarding state to allow seamless resume
5
+ class SessionManager
6
+ SESSION_KEY = "rails_onboarding_session"
7
+ SESSION_TIMEOUT = 2.hours
8
+
9
+ class << self
10
+ # Initialize or restore onboarding session
11
+ #
12
+ # @param user [User] The current user
13
+ # @param session [ActionDispatch::Session] Rails session
14
+ # @return [Hash] Session data
15
+ def initialize_session(user, session)
16
+ session_data = restore_session(user, session)
17
+
18
+ # If no existing session or expired, create new one
19
+ if session_data.nil? || session_expired?(session_data)
20
+ session_data = create_session(user)
21
+ end
22
+
23
+ # Update last activity
24
+ session_data[:last_activity_at] = Time.current.iso8601
25
+ persist_session(user, session, session_data)
26
+
27
+ session_data
28
+ end
29
+
30
+ # Create a new onboarding session
31
+ def create_session(user)
32
+ {
33
+ user_id: user.id,
34
+ started_at: Time.current.iso8601,
35
+ last_activity_at: Time.current.iso8601,
36
+ current_step: user.onboarding_current_step,
37
+ step_history: [],
38
+ form_data: {},
39
+ progress: user.onboarding_progress,
40
+ session_id: SecureRandom.uuid
41
+ }
42
+ end
43
+
44
+ # Restore session from storage
45
+ def restore_session(user, session)
46
+ # Try session storage first
47
+ session_data = session[SESSION_KEY]
48
+ return parse_session_data(session_data) if session_data
49
+
50
+ # Try database storage if available
51
+ if user.respond_to?(:onboarding_session_data)
52
+ parse_session_data(user.onboarding_session_data)
53
+ end
54
+ end
55
+
56
+ # Persist session to storage
57
+ def persist_session(user, session, session_data)
58
+ # Store in Rails session
59
+ session[SESSION_KEY] = session_data.to_json
60
+
61
+ # Also persist to database if available
62
+ if user.respond_to?(:onboarding_session_data=)
63
+ user.onboarding_session_data = session_data.to_json
64
+ user.save(validate: false) rescue nil
65
+ end
66
+ end
67
+
68
+ # Update current step in session
69
+ def update_step(user, session, step_name)
70
+ session_data = initialize_session(user, session)
71
+
72
+ # Add previous step to history
73
+ if session_data[:current_step] && session_data[:current_step] != step_name
74
+ session_data[:step_history] ||= []
75
+ session_data[:step_history] << {
76
+ step: session_data[:current_step],
77
+ completed_at: Time.current.iso8601
78
+ }
79
+ end
80
+
81
+ session_data[:current_step] = step_name.to_s
82
+ session_data[:progress] = user.onboarding_progress
83
+ session_data[:last_activity_at] = Time.current.iso8601
84
+
85
+ persist_session(user, session, session_data)
86
+ session_data
87
+ end
88
+
89
+ # Save form data for a step
90
+ def save_step_data(user, session, step_name, data)
91
+ session_data = initialize_session(user, session)
92
+ session_data[:form_data] ||= {}
93
+ session_data[:form_data][step_name.to_sym] = {
94
+ data: data,
95
+ saved_at: Time.current.iso8601
96
+ }
97
+ persist_session(user, session, session_data)
98
+ end
99
+
100
+ # Get saved form data for a step
101
+ def get_step_data(user, session, step_name)
102
+ session_data = restore_session(user, session)
103
+ return nil unless session_data
104
+
105
+ step_data = session_data.dig(:form_data, step_name.to_sym)
106
+ step_data ? step_data[:data] : nil
107
+ end
108
+
109
+ # Clear step data
110
+ def clear_step_data(user, session, step_name)
111
+ session_data = initialize_session(user, session)
112
+ session_data[:form_data]&.delete(step_name.to_sym)
113
+ persist_session(user, session, session_data)
114
+ end
115
+
116
+ # Get step history
117
+ def step_history(user, session)
118
+ session_data = restore_session(user, session)
119
+ return [] unless session_data
120
+ session_data[:step_history] || []
121
+ end
122
+
123
+ # Get previous step from history
124
+ def previous_step(user, session)
125
+ history = step_history(user, session)
126
+ return nil if history.empty?
127
+ history.last[:step]
128
+ end
129
+
130
+ # Check if session is expired
131
+ def session_expired?(session_data)
132
+ return true unless session_data&.dig(:last_activity_at)
133
+
134
+ last_activity = Time.parse(session_data[:last_activity_at])
135
+ Time.current - last_activity > SESSION_TIMEOUT
136
+ rescue StandardError
137
+ true
138
+ end
139
+
140
+ # Clear session data
141
+ def clear_session(user, session)
142
+ session.delete(SESSION_KEY)
143
+
144
+ if user.respond_to?(:onboarding_session_data=)
145
+ user.onboarding_session_data = nil
146
+ user.save(validate: false) rescue nil
147
+ end
148
+ end
149
+
150
+ # Resume from saved session
151
+ def resume_session(user, session)
152
+ session_data = restore_session(user, session)
153
+
154
+ return nil if session_data.nil? || session_expired?(session_data)
155
+
156
+ # Sync user state with session
157
+ if user.onboarding_current_step != session_data[:current_step]
158
+ user.update(onboarding_current_step: session_data[:current_step]) rescue nil
159
+ end
160
+
161
+ session_data
162
+ end
163
+
164
+ # Get session ID for analytics tracking
165
+ def session_id(user, session)
166
+ session_data = restore_session(user, session)
167
+ session_data&.dig(:session_id) || SecureRandom.uuid
168
+ end
169
+
170
+ private
171
+
172
+ def parse_session_data(data)
173
+ return nil unless data
174
+
175
+ parsed = if data.is_a?(String)
176
+ JSON.parse(data)
177
+ else
178
+ data
179
+ end
180
+
181
+ parsed.deep_symbolize_keys
182
+ rescue JSON::ParserError
183
+ nil
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,159 @@
1
+ module RailsOnboarding
2
+ # Skip Logic Service
3
+ # Allows conditional step skipping based on user data and custom conditions
4
+ class SkipLogic
5
+ class << self
6
+ # Check if a step should be skipped for a user
7
+ #
8
+ # @param user [User] The current user
9
+ # @param step [Hash] The step configuration
10
+ # @return [Boolean] True if step should be skipped
11
+ def should_skip_step?(user, step)
12
+ return false unless step.is_a?(Hash)
13
+
14
+ # Check if step has skip conditions
15
+ skip_conditions = step[:skip_if]
16
+ return false unless skip_conditions
17
+
18
+ evaluate_conditions(user, skip_conditions)
19
+ end
20
+
21
+ # Evaluate skip conditions
22
+ #
23
+ # @param user [User] The user object
24
+ # @param conditions [Hash, Proc, Symbol] The conditions to evaluate
25
+ # @return [Boolean] True if conditions are met
26
+ def evaluate_conditions(user, conditions)
27
+ case conditions
28
+ when Proc
29
+ # Call proc with user
30
+ conditions.call(user)
31
+ when Symbol
32
+ # Call method on user
33
+ user.respond_to?(conditions) && user.send(conditions)
34
+ when Hash
35
+ # Evaluate hash conditions
36
+ evaluate_hash_conditions(user, conditions)
37
+ else
38
+ false
39
+ end
40
+ rescue StandardError => e
41
+ Rails.logger.error("Error evaluating skip conditions: #{e.message}")
42
+ false
43
+ end
44
+
45
+ # Get next unskipped step
46
+ #
47
+ # @param user [User] The current user
48
+ # @param current_step_name [Symbol] Current step name
49
+ # @return [Hash, nil] Next unskipped step
50
+ def next_unskipped_step(user, current_step_name)
51
+ current_index = RailsOnboarding.configuration.step_index(current_step_name)
52
+ return nil unless current_index
53
+
54
+ steps = RailsOnboarding.configuration.steps
55
+ remaining_steps = steps[(current_index + 1)..-1]
56
+
57
+ return nil unless remaining_steps
58
+
59
+ remaining_steps.find { |step| !should_skip_step?(user, step) }
60
+ end
61
+
62
+ # Get all steps that should be skipped for a user
63
+ #
64
+ # @param user [User] The current user
65
+ # @return [Array<Hash>] Array of steps to skip
66
+ def skippable_steps(user)
67
+ RailsOnboarding.configuration.steps.select do |step|
68
+ should_skip_step?(user, step)
69
+ end
70
+ end
71
+
72
+ # Get all steps that are required for a user
73
+ #
74
+ # @param user [User] The current user
75
+ # @return [Array<Hash>] Array of required steps
76
+ def required_steps(user)
77
+ RailsOnboarding.configuration.steps.reject do |step|
78
+ should_skip_step?(user, step)
79
+ end
80
+ end
81
+
82
+ # Calculate actual progress excluding skipped steps
83
+ #
84
+ # @param user [User] The current user
85
+ # @return [Integer] Progress percentage
86
+ def progress_excluding_skipped(user)
87
+ required = required_steps(user)
88
+ return 100 if required.empty?
89
+
90
+ current_index = required.find_index { |s| s[:name] == user.onboarding_current_step&.to_sym }
91
+ return 0 unless current_index
92
+
93
+ ((current_index + 1).to_f / required.size * 100).round
94
+ end
95
+
96
+ # Check if user should auto-skip to next step
97
+ #
98
+ # @param user [User] The current user
99
+ # @param step_name [Symbol] The step to check
100
+ # @return [Hash, nil] Next step if should auto-skip, nil otherwise
101
+ def auto_skip_step(user, step_name)
102
+ step = RailsOnboarding.configuration.step_by_name(step_name)
103
+ return nil unless step
104
+
105
+ if should_skip_step?(user, step) && step[:auto_skip]
106
+ next_unskipped_step(user, step_name)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ # Evaluate hash-based conditions
113
+ def evaluate_hash_conditions(user, conditions)
114
+ operator = conditions[:operator] || :all
115
+
116
+ predicates = conditions.except(:operator).map do |key, value|
117
+ evaluate_predicate(user, key, value)
118
+ end
119
+
120
+ case operator
121
+ when :all
122
+ predicates.all?
123
+ when :any
124
+ predicates.any?
125
+ when :none
126
+ predicates.none?
127
+ else
128
+ predicates.all?
129
+ end
130
+ end
131
+
132
+ # Evaluate a single predicate
133
+ def evaluate_predicate(user, key, value)
134
+ case key
135
+ when :has_attribute
136
+ user.respond_to?(value) && user.send(value).present?
137
+ when :missing_attribute
138
+ !user.respond_to?(value) || user.send(value).blank?
139
+ when :attribute_equals
140
+ attribute, expected = value.is_a?(Hash) ? value.first : [ value, true ]
141
+ user.respond_to?(attribute) && user.send(attribute) == expected
142
+ when :attribute_not_equals
143
+ attribute, expected = value.is_a?(Hash) ? value.first : [ value, true ]
144
+ !user.respond_to?(attribute) || user.send(attribute) != expected
145
+ when :has_role
146
+ user.respond_to?(:has_role?) && user.has_role?(value)
147
+ when :custom
148
+ value.is_a?(Proc) ? value.call(user) : false
149
+ else
150
+ # Try to call as method on user
151
+ user.respond_to?(key) && user.send(key) == value
152
+ end
153
+ rescue StandardError => e
154
+ Rails.logger.error("Error evaluating predicate #{key}: #{e.message}")
155
+ false
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Comprehensive Turbo and Stimulus compatibility for Rails 7+
5
+ # Provides seamless integration with Hotwire stack
6
+ module TurboCompatibility
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Enable Turbo Stream responses
11
+ before_action :set_turbo_frame_headers, if: :turbo_frame_request?
12
+ after_action :turbo_stream_response_cleanup, if: :turbo_stream_request?
13
+ end
14
+
15
+ module ClassMethods
16
+ # Check if Turbo is available
17
+ def turbo_available?
18
+ defined?(Turbo)
19
+ end
20
+
21
+ # Check if Stimulus is available
22
+ def stimulus_available?
23
+ defined?(Stimulus)
24
+ end
25
+
26
+ # Configure Turbo integration options
27
+ def configure_turbo_integration(options = {})
28
+ @turbo_integration_options = {
29
+ enable_streams: true,
30
+ enable_native_navigation: true,
31
+ enable_morphing: options.fetch(:enable_morphing, false),
32
+ broadcast_updates: options.fetch(:broadcast_updates, true)
33
+ }.merge(options)
34
+ end
35
+
36
+ def turbo_integration_options
37
+ @turbo_integration_options || {}
38
+ end
39
+ end
40
+
41
+ # Turbo Frame helpers
42
+ def turbo_frame_request?
43
+ request.headers["Turbo-Frame"].present?
44
+ end
45
+
46
+ def turbo_frame_request_id
47
+ request.headers["Turbo-Frame"]
48
+ end
49
+
50
+ def turbo_stream_request?
51
+ request.format == :turbo_stream ||
52
+ request.headers["Accept"]&.include?("text/vnd.turbo-stream.html")
53
+ end
54
+
55
+ def turbo_native_app?
56
+ request.user_agent.to_s.match?(/Turbo Native/)
57
+ end
58
+
59
+ # Respond with appropriate format for Turbo
60
+ def respond_with_turbo(options = {}, &block)
61
+ respond_to do |format|
62
+ format.html do
63
+ if turbo_frame_request?
64
+ render options.merge(layout: false)
65
+ else
66
+ render options
67
+ end
68
+ end
69
+
70
+ format.turbo_stream do
71
+ if block_given?
72
+ yield
73
+ else
74
+ render turbo_stream: turbo_stream_actions(options)
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Generate Turbo Stream actions
81
+ def turbo_stream_actions(options = {})
82
+ actions = []
83
+
84
+ if options[:replace]
85
+ actions << turbo_stream.replace(options[:replace], partial: options[:partial])
86
+ end
87
+
88
+ if options[:update]
89
+ actions << turbo_stream.update(options[:update], partial: options[:partial])
90
+ end
91
+
92
+ if options[:append]
93
+ actions << turbo_stream.append(options[:append], partial: options[:partial])
94
+ end
95
+
96
+ if options[:prepend]
97
+ actions << turbo_stream.prepend(options[:prepend], partial: options[:partial])
98
+ end
99
+
100
+ if options[:remove]
101
+ actions << turbo_stream.remove(options[:remove])
102
+ end
103
+
104
+ actions
105
+ end
106
+
107
+ # Broadcast onboarding updates via Turbo Streams
108
+ def broadcast_onboarding_update(user, action, data = {})
109
+ return unless self.class.turbo_integration_options[:broadcast_updates]
110
+ return unless defined?(Turbo::StreamsChannel)
111
+
112
+ Turbo::StreamsChannel.broadcast_update_to(
113
+ "onboarding_#{user.id}",
114
+ target: "onboarding-container",
115
+ partial: "rails_onboarding/onboarding/#{action}",
116
+ locals: { user: user, data: data }
117
+ )
118
+ end
119
+
120
+ # Helper to safely navigate with Turbo
121
+ def turbo_navigate_to(path, options = {})
122
+ if turbo_stream_request?
123
+ turbo_stream.action(:redirect, path)
124
+ else
125
+ redirect_to path, options
126
+ end
127
+ end
128
+
129
+ # Turbo Morphing support (for Turbo 8+)
130
+ def enable_turbo_morphing
131
+ response.headers["X-Turbo-Morph"] = "true" if turbo_morph_supported?
132
+ end
133
+
134
+ def turbo_morph_supported?
135
+ return false unless defined?(Turbo)
136
+ # Check if Turbo version supports morphing (8.0+)
137
+ Turbo::VERSION.to_f >= 8.0 rescue false
138
+ end
139
+
140
+ # Stimulus controller data attributes
141
+ def stimulus_controller_data(controller_name, values = {})
142
+ data = { controller: "rails-onboarding--#{controller_name}" }
143
+
144
+ values.each do |key, value|
145
+ data[:"rails-onboarding--#{controller_name}-#{key}-value"] = value
146
+ end
147
+
148
+ data
149
+ end
150
+
151
+ # Generate Stimulus action attributes
152
+ def stimulus_action(event, controller, method, params = {})
153
+ action_string = "#{event}->rails-onboarding--#{controller}##{method}"
154
+
155
+ unless params.empty?
156
+ param_string = params.map { |k, v| "#{k}:#{v}" }.join(" ")
157
+ action_string += "(#{param_string})"
158
+ end
159
+
160
+ { action: action_string }
161
+ end
162
+
163
+ # Turbo Frame tag helper
164
+ def turbo_frame_tag_for_onboarding(id, **options, &block)
165
+ if defined?(turbo_frame_tag)
166
+ turbo_frame_tag(id, **options, &block)
167
+ else
168
+ content_tag(:turbo-frame, id: id, **options, &block)
169
+ end
170
+ end
171
+
172
+ # Turbo Stream tag helper
173
+ def turbo_stream_tag_for_onboarding(action, target, **options, &block)
174
+ if defined?(turbo_stream)
175
+ turbo_stream.send(action, target, **options, &block)
176
+ else
177
+ # Fallback for older versions
178
+ content_tag("turbo-stream", action: action, target: target, **options, &block)
179
+ end
180
+ end
181
+
182
+ # Handle Turbo Confirm dialogs
183
+ def turbo_confirm(message)
184
+ { 'data-turbo-confirm': message }
185
+ end
186
+
187
+ # Handle Turbo Method overrides
188
+ def turbo_method(method)
189
+ { 'data-turbo-method': method }
190
+ end
191
+
192
+ # Disable Turbo for specific links/forms
193
+ def disable_turbo
194
+ { 'data-turbo': "false" }
195
+ end
196
+
197
+ # Enable Turbo permanent elements
198
+ def turbo_permanent
199
+ { 'data-turbo-permanent': "true" }
200
+ end
201
+
202
+ private
203
+
204
+ def set_turbo_frame_headers
205
+ response.headers["Turbo-Frame"] = turbo_frame_request_id if turbo_frame_request_id
206
+ end
207
+
208
+ def turbo_stream_response_cleanup
209
+ # Ensure proper content type for Turbo Stream responses
210
+ response.content_type = "text/vnd.turbo-stream.html" if turbo_stream_request?
211
+ end
212
+ end
213
+
214
+ # Railtie for Turbo/Stimulus integration
215
+ class TurboRailtie < Rails::Railtie
216
+ initializer "rails_onboarding.turbo_compatibility", after: :load_config_initializers do
217
+ ActiveSupport.on_load(:action_controller) do
218
+ if defined?(Turbo) || defined?(Stimulus)
219
+ Rails.logger.info "RailsOnboarding: Turbo/Stimulus compatibility enabled"
220
+
221
+ # Add Turbo Stream MIME type if not already registered
222
+ unless Mime::Type.lookup_by_extension(:turbo_stream)
223
+ Mime::Type.register "text/vnd.turbo-stream.html", :turbo_stream
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,3 @@
1
+ module RailsOnboarding
2
+ VERSION = "0.6.1"
3
+ end
@@ -0,0 +1,44 @@
1
+ require "pagy"
2
+
3
+ require "rails_onboarding/version"
4
+ require "rails_onboarding/engine"
5
+ require "rails_onboarding/deprecation"
6
+ require "rails_onboarding/current"
7
+ require "rails_onboarding/configuration_errors"
8
+ require "rails_onboarding/configuration_validator"
9
+ require "rails_onboarding/configuration"
10
+ require "rails_onboarding/requirements_validator"
11
+ require "rails_onboarding/controller_helpers"
12
+ require "rails_onboarding/responsive_helper"
13
+ require "rails_onboarding/error_recovery"
14
+ require "rails_onboarding/session_manager"
15
+ require "rails_onboarding/skip_logic"
16
+ require "rails_onboarding/multi_tenant"
17
+ require "rails_onboarding/devise_integration"
18
+ require "rails_onboarding/turbo_compatibility"
19
+ require "rails_onboarding/api_mode"
20
+ require "rails_onboarding/background_jobs"
21
+ require "rails_onboarding/caching"
22
+ require "rails_onboarding/lazy_loading"
23
+ require "rails_onboarding/cdn_support"
24
+ require "rails_onboarding/backfill"
25
+
26
+ module RailsOnboarding
27
+ class Error < StandardError; end
28
+
29
+ class << self
30
+ attr_accessor :configuration
31
+ end
32
+
33
+ def self.configure
34
+ self.configuration ||= Configuration.new
35
+ yield(configuration)
36
+ end
37
+
38
+ def self.reset_configuration!
39
+ self.configuration = Configuration.new
40
+ end
41
+
42
+ # Default configuration
43
+ self.configuration = Configuration.new
44
+ end