rails_onboarding 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. metadata +303 -0
@@ -0,0 +1,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