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,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# API Mode support for headless/API-only applications
|
|
5
|
+
# Provides JSON API endpoints for mobile apps and SPAs
|
|
6
|
+
module ApiMode
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
# Skip CSRF for API requests (only works with ActionController classes)
|
|
11
|
+
begin
|
|
12
|
+
skip_before_action :verify_authenticity_token, if: :api_request?, raise: false
|
|
13
|
+
rescue NoMethodError
|
|
14
|
+
# Not an ActionController class, skip this setup
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set API response format (only works with ActionController classes)
|
|
18
|
+
begin
|
|
19
|
+
before_action :set_api_response_format, if: :api_request?
|
|
20
|
+
rescue NoMethodError
|
|
21
|
+
# Not an ActionController class, skip this setup
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Error handling (only works with ActionController classes)
|
|
25
|
+
begin
|
|
26
|
+
rescue_from StandardError, with: :handle_api_error if :api_request?
|
|
27
|
+
rescue NoMethodError
|
|
28
|
+
# Not an ActionController class, skip this setup
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
# Enable API mode for specific actions
|
|
34
|
+
def enable_api_mode(options = {})
|
|
35
|
+
@api_mode_options = {
|
|
36
|
+
version: options.fetch(:version, "v1"),
|
|
37
|
+
authentication: options.fetch(:authentication, :token),
|
|
38
|
+
serializer: options.fetch(:serializer, :active_model_serializers),
|
|
39
|
+
rate_limiting: options.fetch(:rate_limiting, true),
|
|
40
|
+
pagination: options.fetch(:pagination, true)
|
|
41
|
+
}.merge(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def api_mode_options
|
|
45
|
+
@api_mode_options || {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if API mode is enabled
|
|
49
|
+
def api_mode_enabled?
|
|
50
|
+
@api_mode_options.present?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Detect API requests
|
|
55
|
+
def api_request?
|
|
56
|
+
request.format.json? ||
|
|
57
|
+
request.path.start_with?("/api/") ||
|
|
58
|
+
request.headers["Accept"]&.include?("application/json") ||
|
|
59
|
+
request.headers["Content-Type"]&.include?("application/json")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# API response helpers
|
|
63
|
+
def render_api_success(data, status: :ok, meta: {})
|
|
64
|
+
render json: {
|
|
65
|
+
success: true,
|
|
66
|
+
data: serialize_for_api(data),
|
|
67
|
+
meta: build_api_meta(meta)
|
|
68
|
+
}, status: status
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_api_error(message, status: :unprocessable_entity, errors: {})
|
|
72
|
+
render json: {
|
|
73
|
+
success: false,
|
|
74
|
+
error: {
|
|
75
|
+
message: message,
|
|
76
|
+
details: errors
|
|
77
|
+
},
|
|
78
|
+
meta: build_api_meta
|
|
79
|
+
}, status: status
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Onboarding-specific API methods
|
|
83
|
+
def api_onboarding_status
|
|
84
|
+
render_api_success({
|
|
85
|
+
onboarding_completed: current_user.onboarding_completed?,
|
|
86
|
+
onboarding_skipped: current_user.onboarding_skipped?,
|
|
87
|
+
current_step: current_user.onboarding_current_step,
|
|
88
|
+
progress_percentage: current_user.onboarding_progress_percentage,
|
|
89
|
+
steps: api_format_steps,
|
|
90
|
+
next_step: api_next_step,
|
|
91
|
+
previous_step: api_previous_step
|
|
92
|
+
})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def api_complete_step
|
|
96
|
+
step_name = params[:step_name] || params[:step]
|
|
97
|
+
|
|
98
|
+
if current_user.complete_step(step_name)
|
|
99
|
+
render_api_success({
|
|
100
|
+
message: "Step '#{step_name}' completed successfully",
|
|
101
|
+
current_step: current_user.onboarding_current_step,
|
|
102
|
+
progress_percentage: current_user.onboarding_progress_percentage,
|
|
103
|
+
onboarding_completed: current_user.onboarding_completed?
|
|
104
|
+
})
|
|
105
|
+
else
|
|
106
|
+
render_api_error("Failed to complete step '#{step_name}'", errors: current_user.errors.full_messages)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def api_skip_step
|
|
111
|
+
step_name = params[:step_name] || params[:step]
|
|
112
|
+
|
|
113
|
+
if current_user.skip_step(step_name)
|
|
114
|
+
render_api_success({
|
|
115
|
+
message: "Step '#{step_name}' skipped successfully",
|
|
116
|
+
current_step: current_user.onboarding_current_step,
|
|
117
|
+
progress_percentage: current_user.onboarding_progress_percentage
|
|
118
|
+
})
|
|
119
|
+
else
|
|
120
|
+
render_api_error("Failed to skip step '#{step_name}'", errors: current_user.errors.full_messages)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def api_complete_onboarding
|
|
125
|
+
if current_user.complete_onboarding!
|
|
126
|
+
render_api_success({
|
|
127
|
+
message: "Onboarding completed successfully",
|
|
128
|
+
completed_at: current_user.onboarding_completed_at
|
|
129
|
+
})
|
|
130
|
+
else
|
|
131
|
+
render_api_error("Failed to complete onboarding", errors: current_user.errors.full_messages)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def api_restart_onboarding
|
|
136
|
+
if current_user.restart_onboarding!
|
|
137
|
+
render_api_success({
|
|
138
|
+
message: "Onboarding restarted successfully",
|
|
139
|
+
current_step: current_user.onboarding_current_step,
|
|
140
|
+
progress_percentage: 0
|
|
141
|
+
})
|
|
142
|
+
else
|
|
143
|
+
render_api_error("Failed to restart onboarding", errors: current_user.errors.full_messages)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def api_tooltips_list
|
|
148
|
+
tooltips = current_user.available_tooltips
|
|
149
|
+
|
|
150
|
+
render_api_success({
|
|
151
|
+
tooltips: tooltips.map do |tooltip|
|
|
152
|
+
{
|
|
153
|
+
id: tooltip[:id],
|
|
154
|
+
title: tooltip[:title],
|
|
155
|
+
content: tooltip[:content],
|
|
156
|
+
target: tooltip[:target],
|
|
157
|
+
position: tooltip[:position],
|
|
158
|
+
shown: current_user.tooltip_shown?(tooltip[:id])
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
})
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def api_dismiss_tooltip
|
|
165
|
+
tooltip_id = params[:tooltip_id]
|
|
166
|
+
|
|
167
|
+
if current_user.dismiss_tooltip(tooltip_id)
|
|
168
|
+
render_api_success({
|
|
169
|
+
message: "Tooltip dismissed successfully",
|
|
170
|
+
tooltip_id: tooltip_id
|
|
171
|
+
})
|
|
172
|
+
else
|
|
173
|
+
render_api_error("Failed to dismiss tooltip")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# API authentication
|
|
178
|
+
def authenticate_api_request!
|
|
179
|
+
return if current_user.present?
|
|
180
|
+
|
|
181
|
+
token = extract_api_token
|
|
182
|
+
|
|
183
|
+
if token.blank?
|
|
184
|
+
render_api_error("Missing authentication token", status: :unauthorized)
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
user = authenticate_with_token(token)
|
|
189
|
+
|
|
190
|
+
if user
|
|
191
|
+
# Set current_user for the request
|
|
192
|
+
instance_variable_set(:@current_user, user)
|
|
193
|
+
else
|
|
194
|
+
render_api_error("Invalid authentication token", status: :unauthorized)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Current user accessor for API requests
|
|
199
|
+
def current_user
|
|
200
|
+
@current_user ||= begin
|
|
201
|
+
token = extract_api_token
|
|
202
|
+
authenticate_with_token(token) if token.present?
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
# Serialize data for API response
|
|
209
|
+
def serialize_for_api(data)
|
|
210
|
+
case self.class.api_mode_options[:serializer]
|
|
211
|
+
when :active_model_serializers
|
|
212
|
+
# Use ActiveModel::Serializers if available
|
|
213
|
+
if defined?(ActiveModel::Serializer)
|
|
214
|
+
ActiveModelSerializers::SerializableResource.new(data).as_json
|
|
215
|
+
else
|
|
216
|
+
data.as_json
|
|
217
|
+
end
|
|
218
|
+
when :jsonapi
|
|
219
|
+
# Use JSONAPI::Serializer if available
|
|
220
|
+
if defined?(JSONAPI::Serializer)
|
|
221
|
+
data.to_json
|
|
222
|
+
else
|
|
223
|
+
data.as_json
|
|
224
|
+
end
|
|
225
|
+
else
|
|
226
|
+
# Default to as_json
|
|
227
|
+
data.respond_to?(:as_json) ? data.as_json : data
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Build API metadata
|
|
232
|
+
def build_api_meta(custom_meta = {})
|
|
233
|
+
{
|
|
234
|
+
timestamp: Time.current.iso8601,
|
|
235
|
+
version: self.class.api_mode_options[:version] || "v1",
|
|
236
|
+
request_id: request.request_id
|
|
237
|
+
}.merge(custom_meta)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Format steps for API
|
|
241
|
+
def api_format_steps
|
|
242
|
+
RailsOnboarding.configuration.steps.map do |step|
|
|
243
|
+
{
|
|
244
|
+
name: step[:name],
|
|
245
|
+
title: step[:title],
|
|
246
|
+
description: step[:description],
|
|
247
|
+
icon: step[:icon],
|
|
248
|
+
skippable: step[:skippable],
|
|
249
|
+
completed: current_user.step_completed?(step[:name])
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Get next step for API
|
|
255
|
+
def api_next_step
|
|
256
|
+
next_step = current_user.next_step
|
|
257
|
+
return nil unless next_step
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
name: next_step[:name],
|
|
261
|
+
title: next_step[:title],
|
|
262
|
+
description: next_step[:description]
|
|
263
|
+
}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Get previous step for API
|
|
267
|
+
def api_previous_step
|
|
268
|
+
prev_step = current_user.previous_step
|
|
269
|
+
return nil unless prev_step
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
name: prev_step[:name],
|
|
273
|
+
title: prev_step[:title],
|
|
274
|
+
description: prev_step[:description]
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Extract the API token from the request. Tokens are only read from
|
|
279
|
+
# request headers, never from query parameters: a token in the URL leaks
|
|
280
|
+
# into server and proxy access logs, browser history, and the Referer
|
|
281
|
+
# header sent to third parties.
|
|
282
|
+
def extract_api_token
|
|
283
|
+
authorization = request.headers["Authorization"]
|
|
284
|
+
# Authorization: Bearer <token> (also tolerates a bare token value)
|
|
285
|
+
return authorization.split(" ").last if authorization.present?
|
|
286
|
+
|
|
287
|
+
# Custom header for clients that can't set Authorization
|
|
288
|
+
request.headers["X-API-Token"].presence
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Authenticate user with token
|
|
292
|
+
def authenticate_with_token(token)
|
|
293
|
+
# This should be implemented by the host application
|
|
294
|
+
# Default implementation uses a simple token check
|
|
295
|
+
user_class = RailsOnboarding.configuration.user_class_name.constantize
|
|
296
|
+
|
|
297
|
+
if user_class.respond_to?(:find_by_api_token)
|
|
298
|
+
user_class.find_by_api_token(token)
|
|
299
|
+
elsif user_class.column_names.include?("api_token")
|
|
300
|
+
user_class.find_by(api_token: token)
|
|
301
|
+
else
|
|
302
|
+
# Raise error if no token authentication method is available
|
|
303
|
+
raise NotImplementedError,
|
|
304
|
+
"API token authentication not configured. Please add an 'api_token' column to your User model " \
|
|
305
|
+
"or implement a custom 'find_by_api_token' class method. See documentation for more details."
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Set API response format
|
|
310
|
+
def set_api_response_format
|
|
311
|
+
request.format = :json
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Handle API errors
|
|
315
|
+
def handle_api_error(exception)
|
|
316
|
+
Rails.logger.error("API Error: #{exception.class} - #{exception.message}")
|
|
317
|
+
Rails.logger.error(exception.backtrace.join("\n"))
|
|
318
|
+
|
|
319
|
+
render_api_error(
|
|
320
|
+
"An error occurred: #{exception.message}",
|
|
321
|
+
status: :internal_server_error
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# API Controller for dedicated API endpoints
|
|
327
|
+
class ApiController < ActionController::API
|
|
328
|
+
include RailsOnboarding::ApiMode
|
|
329
|
+
|
|
330
|
+
before_action :authenticate_api_request!
|
|
331
|
+
|
|
332
|
+
# GET /api/onboarding/status
|
|
333
|
+
def status
|
|
334
|
+
api_onboarding_status
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# POST /api/onboarding/steps/:step_name/complete
|
|
338
|
+
def complete_step
|
|
339
|
+
api_complete_step
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# POST /api/onboarding/steps/:step_name/skip
|
|
343
|
+
def skip_step
|
|
344
|
+
api_skip_step
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# POST /api/onboarding/complete
|
|
348
|
+
def complete
|
|
349
|
+
api_complete_onboarding
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# POST /api/onboarding/restart
|
|
353
|
+
def restart
|
|
354
|
+
api_restart_onboarding
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# GET /api/onboarding/tooltips
|
|
358
|
+
def tooltips
|
|
359
|
+
api_tooltips_list
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# POST /api/onboarding/tooltips/:tooltip_id/dismiss
|
|
363
|
+
def dismiss_tooltip
|
|
364
|
+
api_dismiss_tooltip
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# One-shot data fixes for applications that installed RailsOnboarding on top
|
|
5
|
+
# of an existing users table.
|
|
6
|
+
#
|
|
7
|
+
# Users who signed up before the gem was installed have NULL onboarding
|
|
8
|
+
# columns, so the admin reports them as "Not Started" and - depending on the
|
|
9
|
+
# +onboarding_required_for+ strategy - they can be pushed back through a flow
|
|
10
|
+
# they have no business seeing. Backfilling marks them as already onboarded.
|
|
11
|
+
#
|
|
12
|
+
# Everything here writes with +update_all+ in batches: no callbacks, no
|
|
13
|
+
# validations, and deliberately no analytics events. Backfilled users never
|
|
14
|
+
# went through onboarding, so recording completion events for them would put
|
|
15
|
+
# fiction into the funnel metrics.
|
|
16
|
+
module Backfill
|
|
17
|
+
DEFAULT_BATCH_SIZE = 1_000
|
|
18
|
+
|
|
19
|
+
REQUIRED_COLUMNS = %w[
|
|
20
|
+
onboarding_completed
|
|
21
|
+
onboarding_completed_at
|
|
22
|
+
onboarding_current_step
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
Result = Struct.new(:matched, :updated, :dry_run, keyword_init: true) do
|
|
26
|
+
def dry_run?
|
|
27
|
+
dry_run
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Marks pre-existing users as having finished onboarding.
|
|
33
|
+
#
|
|
34
|
+
# Only users who have never engaged with onboarding are touched: not
|
|
35
|
+
# completed, not skipped, and sitting on no current step. Anyone
|
|
36
|
+
# mid-flow is left alone so a backfill can be re-run safely while the
|
|
37
|
+
# app is live.
|
|
38
|
+
#
|
|
39
|
+
# @param created_before [Time, Date, String, nil] restrict to users
|
|
40
|
+
# created before this moment. Users with a NULL +created_at+ are
|
|
41
|
+
# always included when a cutoff is given - a row with no creation
|
|
42
|
+
# timestamp predates any cutoff worth naming, and those rows are
|
|
43
|
+
# exactly the legacy ones this task exists for.
|
|
44
|
+
# @param completed_at [Time] timestamp used for users whose +created_at+
|
|
45
|
+
# is NULL. Users with a +created_at+ get their own signup time, which
|
|
46
|
+
# keeps completion-over-time charts from spiking on backfill day.
|
|
47
|
+
# @param batch_size [Integer] rows per UPDATE statement.
|
|
48
|
+
# @param dry_run [Boolean] count the affected rows without writing.
|
|
49
|
+
# @return [Result]
|
|
50
|
+
def mark_existing_users_onboarded(created_before: nil, completed_at: Time.current,
|
|
51
|
+
batch_size: DEFAULT_BATCH_SIZE, dry_run: false)
|
|
52
|
+
ensure_columns!
|
|
53
|
+
|
|
54
|
+
scope = pending_scope(created_before: created_before)
|
|
55
|
+
matched = scope.count
|
|
56
|
+
return Result.new(matched: matched, updated: 0, dry_run: true) if dry_run
|
|
57
|
+
|
|
58
|
+
updated = 0
|
|
59
|
+
scope.in_batches(of: batch_size) do |batch|
|
|
60
|
+
updated += batch.update_all(completion_assignment(completed_at))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Result.new(matched: matched, updated: updated, dry_run: false)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Users the backfill would touch. Exposed so callers can inspect or
|
|
67
|
+
# further narrow the set before running.
|
|
68
|
+
def pending_scope(created_before: nil)
|
|
69
|
+
scope = user_class
|
|
70
|
+
.where(onboarding_current_step: nil)
|
|
71
|
+
.where(onboarding_completed: [ false, nil ])
|
|
72
|
+
|
|
73
|
+
scope = scope.where(onboarding_skipped: [ false, nil ]) if column?("onboarding_skipped")
|
|
74
|
+
return scope unless created_before
|
|
75
|
+
|
|
76
|
+
cutoff = normalize_time(created_before)
|
|
77
|
+
scope.where("#{quoted_table}.created_at < ? OR #{quoted_table}.created_at IS NULL", cutoff)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def user_class
|
|
81
|
+
RailsOnboarding.configuration.user_class_name.constantize
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def completion_assignment(fallback_completed_at)
|
|
87
|
+
assignments = [
|
|
88
|
+
"onboarding_completed = ?",
|
|
89
|
+
"onboarding_completed_at = COALESCE(#{quoted_table}.created_at, ?)"
|
|
90
|
+
]
|
|
91
|
+
values = [ true, normalize_time(fallback_completed_at) ]
|
|
92
|
+
|
|
93
|
+
# Columns added with `default: false` but no NOT NULL constraint can
|
|
94
|
+
# still hold NULL on rows written before the default landed, which
|
|
95
|
+
# makes `onboarding_skipped?` return nil-ish garbage in the admin.
|
|
96
|
+
if column?("onboarding_skipped")
|
|
97
|
+
assignments << "onboarding_skipped = COALESCE(#{quoted_table}.onboarding_skipped, ?)"
|
|
98
|
+
values << false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
user_class.sanitize_sql_for_assignment([ assignments.join(", "), *values ])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ensure_columns!
|
|
105
|
+
missing = REQUIRED_COLUMNS.reject { |name| column?(name) }
|
|
106
|
+
return if missing.empty?
|
|
107
|
+
|
|
108
|
+
raise ArgumentError,
|
|
109
|
+
"#{user_class.name} is missing onboarding columns: #{missing.join(', ')}. " \
|
|
110
|
+
"Run the RailsOnboarding install migrations first."
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def column?(name)
|
|
114
|
+
user_class.column_names.include?(name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def quoted_table
|
|
118
|
+
user_class.connection.quote_table_name(user_class.table_name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def normalize_time(value)
|
|
122
|
+
case value
|
|
123
|
+
when String then Time.zone ? Time.zone.parse(value) : Time.parse(value)
|
|
124
|
+
when Date then value.to_time
|
|
125
|
+
else value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|