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,153 @@
|
|
|
1
|
+
# lib/rails_onboarding/controller_helpers.rb
|
|
2
|
+
module RailsOnboarding
|
|
3
|
+
module ControllerHelpers
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
helper_method :needs_onboarding?, :onboarding_path
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Configuration at the controller level
|
|
11
|
+
class_methods do
|
|
12
|
+
def skip_onboarding_check(options = {})
|
|
13
|
+
@skip_onboarding_options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def skip_onboarding_for_action?(action)
|
|
17
|
+
return false unless @skip_onboarding_options
|
|
18
|
+
|
|
19
|
+
if @skip_onboarding_options[:only]
|
|
20
|
+
Array(@skip_onboarding_options[:only]).include?(action.to_sym)
|
|
21
|
+
elsif @skip_onboarding_options[:except]
|
|
22
|
+
!Array(@skip_onboarding_options[:except]).include?(action.to_sym)
|
|
23
|
+
else
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def needs_onboarding?
|
|
30
|
+
return false unless user_signed_in?
|
|
31
|
+
return false if on_onboarding_page?
|
|
32
|
+
return false if skip_onboarding_request?
|
|
33
|
+
return false if self.class.skip_onboarding_for_action?(action_name)
|
|
34
|
+
|
|
35
|
+
current_user.needs_onboarding?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_onboarding_page?
|
|
39
|
+
# Deliberately reads the engine proxy, not the onboarding_path method -
|
|
40
|
+
# in engine controllers the engine's own url_helper shadows ours, so
|
|
41
|
+
# relying on method resolution here would be ambiguous. Chomping makes
|
|
42
|
+
# the match segment-aware: the engine root (with or without its
|
|
43
|
+
# trailing slash) and anything under it count, but sibling host paths
|
|
44
|
+
# that merely share the prefix (/onboarding_help when mounted at
|
|
45
|
+
# /onboarding) do not.
|
|
46
|
+
root = rails_onboarding.onboarding_path.chomp("/")
|
|
47
|
+
return true if request.path == root || request.path.start_with?("#{root}/")
|
|
48
|
+
|
|
49
|
+
on_current_step_page?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def skip_onboarding_request?
|
|
53
|
+
request.xhr? ||
|
|
54
|
+
request.format.json? ||
|
|
55
|
+
request.path.start_with?("/api")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The onboarding flow lives at the engine's mount root, so the raw engine
|
|
59
|
+
# helper returns it with a trailing slash ("/onboarding/") like any
|
|
60
|
+
# mounted engine root. Serve host apps the canonical slash-less form for
|
|
61
|
+
# links and redirects.
|
|
62
|
+
def onboarding_path
|
|
63
|
+
path = rails_onboarding.onboarding_path.chomp("/")
|
|
64
|
+
path.empty? ? "/" : path
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Complete the current onboarding step from a host-app controller.
|
|
68
|
+
#
|
|
69
|
+
# Call this from the action that performs the step's real work (e.g.
|
|
70
|
+
# ProfilesController#create after a successful save), then redirect to
|
|
71
|
+
# onboarding_path to advance the user:
|
|
72
|
+
#
|
|
73
|
+
# if advance_onboarding!(:profile)
|
|
74
|
+
# redirect_to onboarding_path
|
|
75
|
+
# else
|
|
76
|
+
# redirect_to @profile
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# Deliberately a no-op unless the named step is the user's *current*
|
|
80
|
+
# step - so when the same action runs outside onboarding (the user edits
|
|
81
|
+
# their profile again next week), the controller behaves normally.
|
|
82
|
+
#
|
|
83
|
+
# @param step_name [Symbol, String] the step this action fulfills
|
|
84
|
+
# @return [Boolean] true if the step was completed and the user advanced
|
|
85
|
+
def advance_onboarding!(step_name)
|
|
86
|
+
return false unless user_signed_in?
|
|
87
|
+
return false unless current_user.respond_to?(:needs_onboarding?)
|
|
88
|
+
return false unless current_user.needs_onboarding?
|
|
89
|
+
|
|
90
|
+
step = current_user.current_onboarding_step
|
|
91
|
+
return false unless step && step[:name].to_sym == step_name.to_sym
|
|
92
|
+
|
|
93
|
+
current_user.complete_onboarding_step!(step[:name])
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Resolve a step's :path option against the host application's routes.
|
|
98
|
+
# Symbols/Strings are sent to the main_app route proxy; Procs are
|
|
99
|
+
# instance_exec'd in the controller context, so a zero-arg lambda can use
|
|
100
|
+
# main_app, current_user, params, etc.:
|
|
101
|
+
#
|
|
102
|
+
# path: :new_profile_path
|
|
103
|
+
# path: -> { main_app.new_post_path(from: "onboarding") }
|
|
104
|
+
def resolve_onboarding_step_path(path)
|
|
105
|
+
path.is_a?(Proc) ? instance_exec(&path) : main_app.public_send(path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# True when the current request is for the page a :path-based step points
|
|
111
|
+
# at, or for another action on that same controller. Folded into
|
|
112
|
+
# on_onboarding_page? (and therefore needs_onboarding?) so a host app's
|
|
113
|
+
# `redirect_to onboarding_path if needs_onboarding?` guard doesn't bounce
|
|
114
|
+
# the user off the very flow the current step sent them to.
|
|
115
|
+
#
|
|
116
|
+
# Two things need covering, not just one:
|
|
117
|
+
# - without the exact-path check, /onboarding redirects to the step
|
|
118
|
+
# page and the step page's own guard redirects straight back, forever.
|
|
119
|
+
# - without the same-controller check, the action that actually
|
|
120
|
+
# performs the step's work (e.g. PATCH /profile for a step whose
|
|
121
|
+
# :path is edit_profile_path) never runs at all: the guard fires as a
|
|
122
|
+
# before_action on *every* controller action, so it intercepts the
|
|
123
|
+
# update/create request and redirects to /onboarding before
|
|
124
|
+
# advance_onboarding! ever gets a chance to complete the step. That
|
|
125
|
+
# action decides independently whether the step completes, so the
|
|
126
|
+
# guard only needs to get out of its way.
|
|
127
|
+
def on_current_step_page?
|
|
128
|
+
return false unless user_signed_in?
|
|
129
|
+
return false unless current_user.respond_to?(:current_onboarding_step)
|
|
130
|
+
|
|
131
|
+
step = current_user.current_onboarding_step
|
|
132
|
+
return false unless step.is_a?(Hash) && step[:path]
|
|
133
|
+
|
|
134
|
+
resolved = resolve_onboarding_step_path(step[:path])
|
|
135
|
+
# Compare paths only - the resolved route may carry a query string
|
|
136
|
+
# (path: -> { main_app.new_post_path(from: "onboarding") }) or be a
|
|
137
|
+
# full URL, while request.path never includes either.
|
|
138
|
+
resolved_path = URI.parse(resolved.to_s).path
|
|
139
|
+
return true if request.path == resolved_path
|
|
140
|
+
|
|
141
|
+
recognized = Rails.application.routes.recognize_path(resolved_path, method: "GET")
|
|
142
|
+
recognized[:controller] == params[:controller]
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
Rails.logger.warn("RailsOnboarding: could not resolve step path for loop guard: #{e.class} - #{e.message}")
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def user_signed_in?
|
|
149
|
+
# Override this or use the host app's method
|
|
150
|
+
defined?(current_user) && current_user.present?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
# Per-request/per-job isolated state.
|
|
3
|
+
#
|
|
4
|
+
# MultiTenant.with_tenant_configuration uses this to apply tenant-specific
|
|
5
|
+
# configuration overrides for the duration of a block without mutating the
|
|
6
|
+
# shared RailsOnboarding.configuration singleton - CurrentAttributes is
|
|
7
|
+
# isolated per thread/fiber and is automatically reset by Rails around each
|
|
8
|
+
# request and job, so concurrent requests on a threaded server can never
|
|
9
|
+
# observe (or clobber) each other's tenant configuration.
|
|
10
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
11
|
+
attribute :tenant_overrides
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Deprecation warnings utility module
|
|
5
|
+
# Provides consistent deprecation warnings across the gem
|
|
6
|
+
module Deprecation
|
|
7
|
+
class << self
|
|
8
|
+
# Issue a deprecation warning
|
|
9
|
+
# @param message [String] The deprecation message
|
|
10
|
+
# @param version [String] The version when the feature will be removed
|
|
11
|
+
def warn(message, version: nil)
|
|
12
|
+
full_message = "[DEPRECATION] #{message}"
|
|
13
|
+
full_message += " It will be removed in version #{version}." if version
|
|
14
|
+
|
|
15
|
+
if defined?(ActiveSupport::Deprecation)
|
|
16
|
+
# Use the default deprecator instance
|
|
17
|
+
deprecator = ActiveSupport::Deprecation._instance || ActiveSupport::Deprecation.new
|
|
18
|
+
deprecator.warn(full_message)
|
|
19
|
+
else
|
|
20
|
+
Rails.logger.warn(full_message) if defined?(Rails)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Deprecate a method call
|
|
25
|
+
# @param old_method [String] The deprecated method name
|
|
26
|
+
# @param new_method [String] The replacement method name
|
|
27
|
+
# @param version [String] The version when it will be removed
|
|
28
|
+
def deprecate_method(old_method, new_method: nil, version: nil)
|
|
29
|
+
message = "#{old_method} is deprecated"
|
|
30
|
+
message += " and will be removed in version #{version}" if version
|
|
31
|
+
message += ". Use #{new_method} instead" if new_method
|
|
32
|
+
message += "."
|
|
33
|
+
|
|
34
|
+
warn(message, version: version)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Deprecate a configuration option
|
|
38
|
+
# @param option [String] The deprecated configuration option
|
|
39
|
+
# @param replacement [String] The replacement option
|
|
40
|
+
# @param version [String] The version when it will be removed
|
|
41
|
+
def deprecate_config(option, replacement: nil, version: nil)
|
|
42
|
+
message = "Configuration option '#{option}' is deprecated"
|
|
43
|
+
message += " and will be removed in version #{version}" if version
|
|
44
|
+
message += ". Use '#{replacement}' instead" if replacement
|
|
45
|
+
message += "."
|
|
46
|
+
|
|
47
|
+
warn(message, version: version)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deprecate a data format
|
|
51
|
+
# @param format [String] The deprecated format description
|
|
52
|
+
# @param new_format [String] The new format description
|
|
53
|
+
# @param version [String] The version when support will be removed
|
|
54
|
+
def deprecate_format(format, new_format: nil, version: nil)
|
|
55
|
+
message = "#{format} is deprecated"
|
|
56
|
+
message += " and support will be removed in version #{version}" if version
|
|
57
|
+
message += ". Please migrate to: #{new_format}" if new_format
|
|
58
|
+
message += "."
|
|
59
|
+
|
|
60
|
+
warn(message, version: version)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Integration module for seamless Devise compatibility
|
|
5
|
+
# Automatically detects Devise and provides integration helpers
|
|
6
|
+
module DeviseIntegration
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
# Detect if Devise is available
|
|
11
|
+
if defined?(Devise)
|
|
12
|
+
# Hook into Devise's after_sign_in callback
|
|
13
|
+
after_action :redirect_to_onboarding_if_needed, only: [ :create ], if: -> { devise_controller? }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
# Check if Devise is present in the application
|
|
19
|
+
def devise_available?
|
|
20
|
+
defined?(Devise)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Configure Devise integration
|
|
24
|
+
def configure_devise_integration(options = {})
|
|
25
|
+
@devise_integration_options = {
|
|
26
|
+
redirect_after_sign_in: true,
|
|
27
|
+
redirect_after_sign_up: true,
|
|
28
|
+
skip_for_admin: options.fetch(:skip_for_admin, true),
|
|
29
|
+
admin_check: options.fetch(:admin_check, ->(user) { user.respond_to?(:admin?) && user.admin? })
|
|
30
|
+
}.merge(options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def devise_integration_options
|
|
34
|
+
@devise_integration_options || {}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Instance methods for Devise integration
|
|
39
|
+
def redirect_to_onboarding_if_needed
|
|
40
|
+
return unless self.class.devise_available?
|
|
41
|
+
return unless current_user
|
|
42
|
+
return if devise_integration_skip_onboarding?
|
|
43
|
+
|
|
44
|
+
if current_user.should_see_onboarding?
|
|
45
|
+
store_location_for(current_user, rails_onboarding.onboarding_path) if defined?(store_location_for)
|
|
46
|
+
redirect_to rails_onboarding.onboarding_path and return
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def devise_integration_skip_onboarding?
|
|
53
|
+
options = self.class.devise_integration_options
|
|
54
|
+
|
|
55
|
+
# Skip for admins if configured
|
|
56
|
+
if options[:skip_for_admin] && options[:admin_check]
|
|
57
|
+
return true if options[:admin_check].call(current_user)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def devise_controller?
|
|
64
|
+
is_a?(Devise::SessionsController) ||
|
|
65
|
+
is_a?(Devise::RegistrationsController) ||
|
|
66
|
+
is_a?(Devise::PasswordsController)
|
|
67
|
+
rescue NameError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Helper module to extend Devise controllers
|
|
73
|
+
module DeviseControllerExtension
|
|
74
|
+
extend ActiveSupport::Concern
|
|
75
|
+
|
|
76
|
+
included do
|
|
77
|
+
# Override Devise's after_sign_in_path_for to redirect to onboarding
|
|
78
|
+
def after_sign_in_path_for(resource)
|
|
79
|
+
if resource.respond_to?(:should_see_onboarding?) && resource.should_see_onboarding?
|
|
80
|
+
rails_onboarding.onboarding_path
|
|
81
|
+
else
|
|
82
|
+
stored_location_for(resource) || super
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Override Devise's after_sign_up_path_for to redirect to onboarding
|
|
87
|
+
def after_sign_up_path_for(resource)
|
|
88
|
+
if resource.respond_to?(:should_see_onboarding?) && resource.should_see_onboarding?
|
|
89
|
+
rails_onboarding.onboarding_path
|
|
90
|
+
else
|
|
91
|
+
stored_location_for(resource) || super
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Override after_inactive_sign_up_path_for (for unconfirmed users)
|
|
96
|
+
def after_inactive_sign_up_path_for(resource)
|
|
97
|
+
if RailsOnboarding.configuration.redirect_unconfirmed_to_onboarding
|
|
98
|
+
# Store that they should see onboarding after confirmation
|
|
99
|
+
session[:pending_onboarding] = true
|
|
100
|
+
super
|
|
101
|
+
else
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Railtie to auto-integrate with Devise when available
|
|
109
|
+
class DeviseRailtie < Rails::Railtie
|
|
110
|
+
initializer "rails_onboarding.devise_integration", after: :load_config_initializers do
|
|
111
|
+
ActiveSupport.on_load(:action_controller) do
|
|
112
|
+
if defined?(Devise)
|
|
113
|
+
# Auto-include DeviseControllerExtension in Devise controllers
|
|
114
|
+
Devise::SessionsController.include(RailsOnboarding::DeviseControllerExtension) rescue nil
|
|
115
|
+
Devise::RegistrationsController.include(RailsOnboarding::DeviseControllerExtension) rescue nil
|
|
116
|
+
|
|
117
|
+
Rails.logger.info "RailsOnboarding: Devise integration enabled"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace RailsOnboarding
|
|
4
|
+
|
|
5
|
+
# Make engine's helpers available to the main app
|
|
6
|
+
config.to_prepare do
|
|
7
|
+
ApplicationController.helper(RailsOnboarding::Engine.helpers)
|
|
8
|
+
ApplicationController.helper(RailsOnboarding::ResponsiveHelper)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Load migrations
|
|
12
|
+
initializer :append_migrations do |app|
|
|
13
|
+
unless app.root.to_s.match?(root.to_s)
|
|
14
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
|
15
|
+
app.config.paths["db/migrate"] << expanded_path
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Load JavaScript and CSS assets
|
|
21
|
+
initializer "rails_onboarding.assets" do |app|
|
|
22
|
+
# Detect which asset pipeline is in use
|
|
23
|
+
asset_pipeline = Engine.detect_asset_pipeline(app)
|
|
24
|
+
|
|
25
|
+
# Configure assets based on the pipeline
|
|
26
|
+
case asset_pipeline
|
|
27
|
+
when :sprockets
|
|
28
|
+
Engine.configure_sprockets_assets(app)
|
|
29
|
+
when :propshaft
|
|
30
|
+
Engine.configure_propshaft_assets(app)
|
|
31
|
+
when :importmap
|
|
32
|
+
Engine.configure_importmap_assets(app)
|
|
33
|
+
else
|
|
34
|
+
# Fallback configuration for modern bundlers (ESBuild, Webpack, etc.)
|
|
35
|
+
Engine.configure_modern_bundler_assets(app)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Rails.logger.info "RailsOnboarding: Detected asset pipeline: #{asset_pipeline}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Add engine's controller methods to ApplicationController
|
|
42
|
+
initializer "rails_onboarding.controller_helpers" do
|
|
43
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
44
|
+
include RailsOnboarding::ControllerHelpers
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add view paths for the engine
|
|
49
|
+
initializer "rails_onboarding.view_paths" do |app|
|
|
50
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
51
|
+
# Add engine views to the view path
|
|
52
|
+
view_path = RailsOnboarding::Engine.root.join("app", "views")
|
|
53
|
+
|
|
54
|
+
# Ensure the view path exists before adding it
|
|
55
|
+
if File.directory?(view_path)
|
|
56
|
+
append_view_path view_path
|
|
57
|
+
Rails.logger.debug "RailsOnboarding: Added view path: #{view_path}"
|
|
58
|
+
else
|
|
59
|
+
Rails.logger.warn "RailsOnboarding: View path not found: #{view_path}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Also add to ActionMailer if available
|
|
64
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
65
|
+
view_path = RailsOnboarding::Engine.root.join("app", "views")
|
|
66
|
+
if File.directory?(view_path)
|
|
67
|
+
append_view_path view_path
|
|
68
|
+
Rails.logger.debug "RailsOnboarding: Added mailer view path: #{view_path}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Optional: Setup Stimulus integration if available
|
|
74
|
+
config.after_initialize do |app|
|
|
75
|
+
Engine.setup_stimulus_integration(app) if Engine.stimulus_available?(app)
|
|
76
|
+
Engine.setup_importmap_integration(app) if Engine.importmap_available?(app)
|
|
77
|
+
|
|
78
|
+
# Bundler apps (esbuild/webpack/vite) vendor a *copy* of the gem's Stimulus
|
|
79
|
+
# controllers, so unlike the engine-served CSS/views they don't auto-update
|
|
80
|
+
# on a gem bump. Nudge the developer when that copy has drifted. Dev only.
|
|
81
|
+
Engine.warn_if_controllers_stale(app) if Rails.env.development?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# NOTE: everything below is internal to the engine, not part of its public API.
|
|
85
|
+
# It can't actually be marked `private` (bare `private` has no effect on `def self.x`
|
|
86
|
+
# methods, and `private_class_method` would break the initializers above: those run
|
|
87
|
+
# via `instance_exec` with the Rails::Railtie *instance* as `self`, not this class, so
|
|
88
|
+
# they must call these as `Engine.method_name(...)` with an explicit receiver).
|
|
89
|
+
|
|
90
|
+
# Detect which asset pipeline is being used
|
|
91
|
+
def self.detect_asset_pipeline(app)
|
|
92
|
+
if defined?(Propshaft)
|
|
93
|
+
:propshaft
|
|
94
|
+
elsif defined?(Sprockets) && app.config.respond_to?(:assets)
|
|
95
|
+
:sprockets
|
|
96
|
+
elsif importmap_available?(app)
|
|
97
|
+
:importmap
|
|
98
|
+
else
|
|
99
|
+
:modern_bundler
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Configure Sprockets-based asset pipeline
|
|
104
|
+
def self.configure_sprockets_assets(app)
|
|
105
|
+
app.config.assets.paths << root.join("app", "assets", "stylesheets")
|
|
106
|
+
app.config.assets.paths << root.join("app", "assets", "javascripts")
|
|
107
|
+
|
|
108
|
+
# Precompile all CSS and JavaScript assets
|
|
109
|
+
app.config.assets.precompile += %w[
|
|
110
|
+
rails_onboarding/application.css
|
|
111
|
+
rails_onboarding/tooltips.css
|
|
112
|
+
rails_onboarding/utilities.css
|
|
113
|
+
rails_onboarding/accessibility.css
|
|
114
|
+
rails_onboarding/tour.css
|
|
115
|
+
rails_onboarding/mobile.css
|
|
116
|
+
rails_onboarding/milestones.css
|
|
117
|
+
rails_onboarding/progressive_disclosure.css
|
|
118
|
+
rails_onboarding/flash_messages.css
|
|
119
|
+
rails_onboarding/admin.css
|
|
120
|
+
rails_onboarding/application.js
|
|
121
|
+
rails_onboarding/*.js
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
Rails.logger.info "RailsOnboarding: Configured Sprockets asset pipeline"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Configure Propshaft-based asset pipeline
|
|
128
|
+
def self.configure_propshaft_assets(app)
|
|
129
|
+
# Propshaft automatically includes all assets from engine paths
|
|
130
|
+
app.config.assets.paths << root.join("app", "assets", "stylesheets")
|
|
131
|
+
app.config.assets.paths << root.join("app", "assets", "javascripts")
|
|
132
|
+
|
|
133
|
+
Rails.logger.info "RailsOnboarding: Configured Propshaft asset pipeline"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Configure Importmap-based asset loading
|
|
137
|
+
def self.configure_importmap_assets(app)
|
|
138
|
+
# Importmap handles JavaScript, but we still need stylesheets
|
|
139
|
+
if app.config.respond_to?(:assets)
|
|
140
|
+
app.config.assets.paths << root.join("app", "assets", "stylesheets")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
Rails.logger.info "RailsOnboarding: Configured Importmap asset loading"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Configure for modern bundlers (ESBuild, Webpack, etc.)
|
|
147
|
+
def self.configure_modern_bundler_assets(app)
|
|
148
|
+
# Add asset paths if assets config is available
|
|
149
|
+
if app.config.respond_to?(:assets)
|
|
150
|
+
app.config.assets.paths << root.join("app", "assets", "stylesheets")
|
|
151
|
+
app.config.assets.paths << root.join("app", "assets", "javascripts")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Rails.logger.info "RailsOnboarding: Configured for modern bundler (ESBuild/Webpack/etc.)"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.stimulus_available?(app)
|
|
158
|
+
defined?(StimulusRails) &&
|
|
159
|
+
app.config.respond_to?(:stimulus) &&
|
|
160
|
+
app.config.stimulus.respond_to?(:paths)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.setup_stimulus_integration(app)
|
|
164
|
+
begin
|
|
165
|
+
app.config.stimulus.paths << root.join("app", "assets", "javascripts", "rails_onboarding")
|
|
166
|
+
Rails.logger.info "RailsOnboarding: Stimulus integration enabled"
|
|
167
|
+
rescue => e
|
|
168
|
+
Rails.logger.warn "RailsOnboarding: Could not setup Stimulus integration: #{e.message}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.importmap_available?(app)
|
|
173
|
+
defined?(Importmap) &&
|
|
174
|
+
defined?(Importmap::Map) &&
|
|
175
|
+
app.config.respond_to?(:importmap)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.setup_importmap_integration(app)
|
|
179
|
+
begin
|
|
180
|
+
importmap_path = root.join("config", "importmap.rb")
|
|
181
|
+
if File.exist?(importmap_path)
|
|
182
|
+
# importmap-rails draws app.config.importmap.paths into app.importmap
|
|
183
|
+
# during its own "importmap" initializer, which has already run by the
|
|
184
|
+
# time this after_initialize hook fires - appending to that array here
|
|
185
|
+
# would be a no-op. Draw directly into the live map instead so the
|
|
186
|
+
# engine's pins actually get registered.
|
|
187
|
+
app.importmap.draw(importmap_path)
|
|
188
|
+
Rails.logger.info "RailsOnboarding: Importmap integration enabled"
|
|
189
|
+
end
|
|
190
|
+
rescue => e
|
|
191
|
+
Rails.logger.warn "RailsOnboarding: Could not setup importmap integration: #{e.message}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Warn (dev only) when a bundler app's vendored copy of the Stimulus
|
|
196
|
+
# controllers has drifted from the installed gem version. `rails g
|
|
197
|
+
# rails_onboarding:update` stamps the copy with a version marker; we compare
|
|
198
|
+
# that marker to RailsOnboarding::VERSION. Checks the flat layout (controllers
|
|
199
|
+
# copied straight into controllers/, the current recommendation) as well as
|
|
200
|
+
# the legacy controllers/rails_onboarding/ subfolder so an un-migrated app
|
|
201
|
+
# still gets nudged. Never raises - a warning must not take down boot.
|
|
202
|
+
def self.warn_if_controllers_stale(app)
|
|
203
|
+
controllers = app.root.join("app", "javascript", "controllers")
|
|
204
|
+
return unless Dir.exist?(controllers)
|
|
205
|
+
|
|
206
|
+
legacy = controllers.join("rails_onboarding")
|
|
207
|
+
marker =
|
|
208
|
+
if File.exist?(controllers.join(".rails_onboarding_version"))
|
|
209
|
+
controllers.join(".rails_onboarding_version")
|
|
210
|
+
elsif File.exist?(legacy.join(".rails_onboarding_version"))
|
|
211
|
+
legacy.join(".rails_onboarding_version")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Is a copy vendored here at all? A marker, the legacy subfolder, or a
|
|
215
|
+
# signature controller (onboarding_controller.js is distinctively ours,
|
|
216
|
+
# so it won't be confused with a host controller in the flat layout).
|
|
217
|
+
vendored = marker ||
|
|
218
|
+
Dir.exist?(legacy) ||
|
|
219
|
+
File.exist?(controllers.join("onboarding_controller.js"))
|
|
220
|
+
return unless vendored
|
|
221
|
+
|
|
222
|
+
current = RailsOnboarding::VERSION
|
|
223
|
+
cmd = "bin/rails generate rails_onboarding:update"
|
|
224
|
+
|
|
225
|
+
unless marker
|
|
226
|
+
Rails.logger.warn(
|
|
227
|
+
"[rails_onboarding] Vendored Stimulus controllers have no version marker, so " \
|
|
228
|
+
"they may be stale relative to gem v#{current}. Run `#{cmd}` to re-sync and " \
|
|
229
|
+
"stamp them, then rebuild your JS bundle."
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
stamped = File.read(marker).strip
|
|
235
|
+
if stamped != current
|
|
236
|
+
Rails.logger.warn(
|
|
237
|
+
"[rails_onboarding] Vendored Stimulus controllers were generated for v#{stamped} " \
|
|
238
|
+
"but the gem is v#{current}. Run `#{cmd}` to re-sync, then rebuild your JS bundle."
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
rescue => e
|
|
242
|
+
Rails.logger.debug "RailsOnboarding: controller staleness check skipped: #{e.message}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|