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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- 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,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
|