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
data/README.md ADDED
@@ -0,0 +1,1351 @@
1
+ # Rails Onboarding
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rails_onboarding.svg)](https://badge.fury.io/rb/rails_onboarding)
4
+ [![Build Status](https://github.com/bunnahabhain/rails_onboarding/workflows/CI/badge.svg)](https://github.com/bunnahabhain/rails_onboarding/actions)
5
+
6
+ A flexible, customizable onboarding engine for Rails applications. Create engaging multi-step onboarding flows with progress tracking, tooltips, milestones, analytics, and more.
7
+
8
+ ## Features
9
+
10
+ - 🎯 **Multi-Step Onboarding Flows** - Guide users through customizable onboarding steps
11
+ - 📊 **Progress Tracking** - Visual progress indicators and completion tracking
12
+ - 💬 **Smart Tooltips** - Context-aware feature tooltips with progressive disclosure
13
+ - 🏆 **Milestone System** - Achievement tracking with points and celebrations
14
+ - 📈 **Analytics & Metrics** - Comprehensive tracking of completion rates, drop-off points, and user engagement
15
+ - 🎨 **Interactive Tours** - Guided walkthroughs with spotlight effects
16
+ - 🧪 **A/B Testing** - Test different onboarding flows and measure effectiveness
17
+ - 👥 **Personalization** - Adapt onboarding based on user type or role
18
+ - 🌍 **Internationalization** - Built-in support for multiple languages
19
+ - 📱 **Mobile Responsive** - Fully responsive design for all devices
20
+ - ⚡ **Performance Optimized** - Caching, database optimization, lazy loading, and CDN support
21
+ - 🔌 **Easy Integration** - Works seamlessly with Devise, Turbo, and Stimulus
22
+ - 🎭 **Multi-Tenant Support** - Different onboarding flows per organization
23
+ - 📦 **Pre-built Templates** - 5 ready-to-use onboarding flows for common use cases
24
+
25
+ ## Table of Contents
26
+
27
+ - [Installation](#installation)
28
+ - [Quick Start](#quick-start)
29
+ - [Configuration](#configuration)
30
+ - [Usage](#usage)
31
+ - [Advanced Features](#advanced-features)
32
+ - [Admin Dashboard](#admin-dashboard)
33
+ - [API Reference](#api-reference)
34
+ - [Testing](#testing)
35
+ - [Contributing](#contributing)
36
+ - [License](#license)
37
+
38
+ ## Installation
39
+
40
+ Add this line to your application's Gemfile:
41
+
42
+ ```ruby
43
+ gem "rails_onboarding"
44
+ ```
45
+
46
+ Then execute:
47
+
48
+ ```bash
49
+ bundle install
50
+ ```
51
+
52
+ Run the installation generator:
53
+
54
+ ```bash
55
+ rails generate rails_onboarding:install
56
+ ```
57
+
58
+ > **Prerequisite:** A `User` model must already exist at `app/models/user.rb`. The generator checks for it and stops with an error if it's missing. Create one first (for example `rails generate model User email:string`) if you don't have it yet.
59
+
60
+ This will:
61
+ - Create an initializer at `config/initializers/rails_onboarding.rb`
62
+ - Generate six migrations: onboarding fields on your `User` model, the analytics event tables, the onboarding flows table, milestone tracking fields, performance indexes, and robustness/error-recovery fields
63
+ - Mount the engine in your routes at `/onboarding`
64
+ - Add a customizable stylesheet at `app/assets/stylesheets/rails_onboarding_custom.css`
65
+
66
+ Run the migrations:
67
+
68
+ ```bash
69
+ bin/rails db:migrate
70
+ ```
71
+
72
+ The `User` migration adds `onboarding_completed`, `onboarding_completed_at`, `onboarding_current_step`, `onboarding_skipped`, and `feature_tooltips_shown` (stored as `jsonb` on PostgreSQL, `json` on MySQL/MariaDB, and serialized `text` elsewhere), along with the indexes needed to query onboarding state efficiently. On PostgreSQL these indexes are built concurrently so the migration won't lock a large, live `users` table.
73
+
74
+ ### Installing into an app that already has users
75
+
76
+ The migration leaves every existing user with empty onboarding columns, so the
77
+ admin reports them all as "Not Started" and — depending on your
78
+ `onboarding_required_for` setting — they may be pushed into a flow they don't
79
+ need. Backfill them once, after migrating:
80
+
81
+ ```bash
82
+ # See what would change first
83
+ bin/rails rails_onboarding:backfill_existing_users DRY_RUN=true
84
+
85
+ # Apply it
86
+ bin/rails rails_onboarding:backfill_existing_users
87
+ ```
88
+
89
+ | Variable | Default | Purpose |
90
+ |----------|---------|---------|
91
+ | `BEFORE` | none | Only backfill users created before this date, e.g. `BEFORE=2026-07-01`. Use it when the gem has been live for a while and you only want to grandfather in the pre-install cohort. Users with a `NULL` `created_at` are always included. |
92
+ | `DRY_RUN` | `false` | Report the count without writing. |
93
+ | `BATCH_SIZE` | `1000` | Rows per `UPDATE`. |
94
+
95
+ Only users who have never engaged with onboarding are touched — not completed,
96
+ not skipped, and on no current step — so anyone mid-flow is left alone and the
97
+ task is safe to re-run on a live app. `onboarding_completed_at` is taken from
98
+ each user's own `created_at` (falling back to now when that is `NULL`), which
99
+ keeps your completion-over-time charts from spiking on backfill day. No
100
+ analytics events are recorded: these users never actually went through
101
+ onboarding, and inventing events would corrupt your funnel metrics.
102
+
103
+ It's also available from the console, where `pending_scope` lets you inspect
104
+ exactly who is about to be affected:
105
+
106
+ ```ruby
107
+ RailsOnboarding::Backfill.pending_scope(created_before: "2026-07-01").pluck(:id, :email)
108
+ RailsOnboarding::Backfill.mark_existing_users_onboarded(created_before: "2026-07-01")
109
+ ```
110
+
111
+ ## Requirements
112
+
113
+ This gem requires:
114
+ 1. **Rails 8.0+** (enforced by the gemspec)
115
+ 2. **Ruby 3.4.9+** (enforced by the gemspec)
116
+ 3. An `ApplicationController` class
117
+ 4. A `current_user` method available in your controllers
118
+ 5. Authentication system (Devise, custom, etc.)
119
+
120
+ ### Version Compatibility Matrix
121
+
122
+ | Rails Version | Ruby Version | rails_onboarding | Status | Notes |
123
+ |--------------|--------------|------------------|--------|-------|
124
+ | 8.0.x | 3.4.9+ | 0.1.0+ | ✅ Supported | Minimum required by the gemspec |
125
+ | < 8.0 | - | - | ❌ Not Supported | Rails 8.0+ required |
126
+ | any | < 3.4.9 | - | ❌ Not Supported | Ruby 3.4.9+ required |
127
+
128
+ **Legend:**
129
+ - ✅ **Supported**: Meets the gemspec's version constraints
130
+ - ❌ **Not Supported**: Will not install (blocked by the gemspec's version constraints)
131
+
132
+ **Feature/Toolchain Compatibility (Rails 8.0+):**
133
+
134
+ | Feature | Supported | Notes |
135
+ |---------|-----------|-------|
136
+ | Core Onboarding | ✅ | |
137
+ | Turbo Integration | ✅ | Via turbo-rails |
138
+ | Stimulus Controllers | ✅ | Via stimulus-rails |
139
+ | Importmap | ✅ | Native support |
140
+ | Asset Pipeline (Sprockets) | ✅ | |
141
+ | Propshaft | ✅ | |
142
+ | ESBuild/Webpack | ✅ | Via jsbundling-rails |
143
+ | PostgreSQL | ✅ | Recommended for JSONB |
144
+ | MySQL/MariaDB | ✅ | JSON field support |
145
+ | SQLite | ✅ | Development/testing only |
146
+
147
+ ### Dependencies
148
+
149
+ The gem has both required and optional dependencies:
150
+
151
+ #### Required Dependencies
152
+
153
+ These dependencies are automatically installed with the gem:
154
+
155
+ | Gem | Version | Purpose |
156
+ |-----|---------|---------|
157
+ | rails | >= 8.0.0 | Core Rails framework |
158
+ | csv | (latest) | Admin CSV exports (no longer a Ruby default gem as of Ruby 3.4) |
159
+
160
+ #### Optional Dependencies
161
+
162
+ These enhance functionality but are not required:
163
+
164
+ | Gem | Version | Purpose | When Needed |
165
+ |-----|---------|---------|-------------|
166
+ | stimulus-rails | >= 1.0.0 | Interactive JavaScript controllers | For Stimulus-based interactivity (tooltips, tours, navigation) |
167
+ | turbo-rails | >= 1.0.0 | Hotwire/Turbo integration | For seamless page transitions and real-time updates |
168
+ | importmap-rails | >= 1.0.0 | JavaScript module loading | If using importmaps for asset management |
169
+ | jsbundling-rails | >= 1.0.0 | JavaScript bundling | If using ESBuild/Webpack for assets |
170
+ | cssbundling-rails | >= 1.0.0 | CSS bundling | If using Tailwind/PostCSS/Sass |
171
+ | propshaft | >= 0.6.0 | Asset pipeline | Alternative to Sprockets for Rails 7+ |
172
+ | sprockets-rails | >= 3.4.0 | Asset pipeline | Traditional asset pipeline support |
173
+ | pg | >= 1.1 | PostgreSQL adapter | For JSONB field support (recommended) |
174
+ | mysql2 | >= 0.5 | MySQL adapter | For JSON field support |
175
+ | sqlite3 | >= 1.4 | SQLite adapter | Development/testing |
176
+ | sidekiq | >= 6.0 | Background jobs | For async email sending |
177
+ | resque | >= 2.0 | Background jobs | Alternative job processor |
178
+ | delayed_job | >= 4.1 | Background jobs | Alternative job processor |
179
+ | redis | >= 4.0 | Caching | For production caching with Redis |
180
+
181
+ #### Installation Recommendations
182
+
183
+ **Minimal Installation (Core Features Only):**
184
+ ```ruby
185
+ # Gemfile
186
+ gem "rails_onboarding"
187
+ ```
188
+
189
+ **Recommended Installation (Full Features):**
190
+ ```ruby
191
+ # Gemfile
192
+ gem "rails_onboarding"
193
+ gem "stimulus-rails"
194
+ gem "turbo-rails"
195
+ gem "importmap-rails" # or jsbundling-rails
196
+ ```
197
+
198
+ **Full-Featured Installation (All Advanced Features):**
199
+ ```ruby
200
+ # Gemfile
201
+ gem "rails_onboarding"
202
+ gem "stimulus-rails"
203
+ gem "turbo-rails"
204
+ gem "importmap-rails"
205
+ gem "sidekiq" # for background jobs
206
+ gem "redis" # for caching
207
+ gem "pg" # for PostgreSQL/JSONB support
208
+ ```
209
+
210
+ #### Feature Requirements
211
+
212
+ Different features require different optional dependencies:
213
+
214
+ | Feature | Required Dependencies | Optional Dependencies |
215
+ |---------|----------------------|----------------------|
216
+ | Core Onboarding | rails | - |
217
+ | Interactive Tooltips | rails | stimulus-rails |
218
+ | Guided Tours | rails | stimulus-rails |
219
+ | Turbo Navigation | rails | turbo-rails, stimulus-rails |
220
+ | Background Emails | rails | sidekiq/resque/delayed_job, actionmailer |
221
+ | Production Caching | rails | redis, actionpack |
222
+ | JSONB Tooltips/Milestones | rails | pg (PostgreSQL) |
223
+ | Analytics Tracking | rails | - |
224
+ | A/B Testing | rails | - |
225
+ | Multi-Tenant | rails | - |
226
+
227
+ #### Checking for Optional Dependencies
228
+
229
+ The gem automatically detects available dependencies:
230
+
231
+ ```ruby
232
+ # Check if Stimulus is available
233
+ RailsOnboarding.stimulus_available?
234
+
235
+ # Check if Turbo is available
236
+ RailsOnboarding.turbo_available?
237
+
238
+ # Check if background jobs are available
239
+ RailsOnboarding.background_jobs_available?
240
+
241
+ # The gem gracefully degrades if optional dependencies are missing
242
+ ```
243
+
244
+ ## Quick Start
245
+
246
+ ### 1. Include the Onboardable Concern
247
+
248
+ Add the concern to your User model:
249
+
250
+ ```ruby
251
+ # app/models/user.rb
252
+ class User < ApplicationRecord
253
+ include RailsOnboarding::Onboardable
254
+
255
+ # Your existing code...
256
+ end
257
+ ```
258
+
259
+ ### 2. Configure Your Onboarding Flow
260
+
261
+ Edit the generated initializer:
262
+
263
+ ```ruby
264
+ # config/initializers/rails_onboarding.rb
265
+ RailsOnboarding.configure do |config|
266
+ config.user_class_name = 'User'
267
+ config.redirect_after_completion = :dashboard_path
268
+ config.redirect_after_skip = :root_path
269
+
270
+ config.steps = [
271
+ { name: :welcome, title: 'Welcome!', icon: '👋', skippable: true },
272
+ { name: :profile, title: 'Setup Profile', icon: '👤', skippable: false },
273
+ { name: :preferences, title: 'Preferences', icon: '⚙️', skippable: true },
274
+ { name: :explore, title: 'Explore Features', icon: '🔍', skippable: true }
275
+ ]
276
+ end
277
+ ```
278
+
279
+ ### 3. Protect Your Controllers
280
+
281
+ `RailsOnboarding::ControllerHelpers` is included into `ActionController::Base`
282
+ automatically by the engine, giving every controller a `needs_onboarding?`
283
+ predicate (it already accounts for XHR/JSON requests, the onboarding page
284
+ itself, and per-action skips - see below). The gem doesn't redirect for you;
285
+ wire up a `before_action` in your own `ApplicationController`:
286
+
287
+ ```ruby
288
+ # app/controllers/application_controller.rb
289
+ class ApplicationController < ActionController::Base
290
+ before_action :redirect_to_onboarding_if_needed
291
+
292
+ private
293
+
294
+ def redirect_to_onboarding_if_needed
295
+ redirect_to onboarding_path if needs_onboarding?
296
+ end
297
+ end
298
+ ```
299
+
300
+ Skip onboarding for specific actions:
301
+
302
+ ```ruby
303
+ class ProfilesController < ApplicationController
304
+ skip_onboarding_check only: [:edit, :update] # or except: [...]
305
+
306
+ def edit
307
+ # Users can access this during onboarding
308
+ end
309
+ end
310
+ ```
311
+
312
+ ### 4. Customize Step Views
313
+
314
+ Create custom views for your onboarding steps:
315
+
316
+ ```erb
317
+ <!-- app/views/rails_onboarding/onboarding/welcome.html.erb -->
318
+ <div class="onboarding-step">
319
+ <h1>Welcome to <%= Rails.application.class.name %>!</h1>
320
+ <p>Let's get you started with a quick tour.</p>
321
+
322
+ <%= link_to "Get Started", next_onboarding_path,
323
+ method: :post,
324
+ class: "btn btn-primary" %>
325
+ </div>
326
+ ```
327
+
328
+ ## Configuration
329
+
330
+ ### Basic Configuration
331
+
332
+ ```ruby
333
+ RailsOnboarding.configure do |config|
334
+ # User model configuration
335
+ config.user_class_name = 'User'
336
+
337
+ # Redirect paths
338
+ config.redirect_after_completion = :dashboard_path
339
+ config.redirect_after_skip = :root_path
340
+
341
+ # Feature flags
342
+ config.enable_tooltips = true
343
+ config.enable_milestones = true
344
+ config.enable_analytics = true
345
+
346
+ # Onboarding requirements
347
+ config.onboarding_required_for = :new_users # or :all_users, :none
348
+
349
+ # Define your onboarding steps
350
+ config.steps = [
351
+ { name: :welcome, title: 'Welcome', icon: '🎉', skippable: true },
352
+ { name: :profile, title: 'Complete Profile', icon: '👤', skippable: false },
353
+ { name: :first_action, title: 'Take Action', icon: '🚀', skippable: false },
354
+ { name: :explore, title: 'Explore', icon: '🔍', skippable: true }
355
+ ]
356
+ end
357
+ ```
358
+
359
+ ### Step Configuration Options
360
+
361
+ Each step supports these options:
362
+
363
+ ```ruby
364
+ {
365
+ name: :step_name, # Required: Unique identifier
366
+ title: 'Step Title', # Required: Display title
367
+ icon: '🎯', # Optional: Emoji or icon class
368
+ skippable: true, # Optional: Can user skip this step?
369
+ description: 'Details...', # Optional: Additional context
370
+ estimated_time: '2 min', # Optional: Time estimate
371
+ required_fields: [:name], # Optional: Required user fields
372
+ condition: ->(user) { ... },# Optional: Show step conditionally
373
+ path: :new_profile_path, # Optional: Host-app page that owns this step
374
+ # (Symbol/String route helper, or a zero-arg
375
+ # Proc evaluated in the controller context)
376
+ complete_if: ->(user) { # Optional: Predicate checked on each visit to
377
+ user.profile.present? # /onboarding - when true, the step is
378
+ } # completed and the user advances
379
+ }
380
+ ```
381
+
382
+ ### Reusing Existing Controllers and Views (`path:` steps)
383
+
384
+ By default each step is rendered by the gem from
385
+ `app/views/rails_onboarding/onboarding/<step_name>.html.erb`. That's right
386
+ for pure-content steps (a welcome screen, a feature tour), but wrong for
387
+ steps your app already has a page for - duplicating your profile form into
388
+ an onboarding template means two copies to keep in sync.
389
+
390
+ Give those steps a `path:` instead. Visiting `/onboarding` then redirects to
391
+ your real page, and your existing controller and view do all the work - the
392
+ gem only tracks progress:
393
+
394
+ ```ruby
395
+ config.steps = [
396
+ { name: :welcome, title: 'Welcome', skippable: true }, # gem-rendered
397
+ { name: :profile, title: 'Set up your profile', icon: '👤',
398
+ path: :new_profile_path,
399
+ complete_if: ->(user) { user.profile.present? } },
400
+ { name: :first_post, title: 'Create your first post',
401
+ path: -> { main_app.new_post_path(from: 'onboarding') },
402
+ complete_if: ->(user) { user.posts.exists? } }
403
+ ]
404
+ ```
405
+
406
+ A `path:` step is completed in one of two ways:
407
+
408
+ 1. **Declaratively** via `complete_if:` - whenever the user lands back on
409
+ `/onboarding`, every satisfied step is skipped automatically. Your
410
+ controllers never have to know onboarding exists.
411
+ 2. **Explicitly** via `advance_onboarding!` in the host controller - useful
412
+ when completion isn't derivable from persisted state:
413
+
414
+ ```ruby
415
+ class ProfilesController < ApplicationController
416
+ def create
417
+ @profile = current_user.build_profile(profile_params)
418
+ if @profile.save
419
+ return redirect_to onboarding_path if advance_onboarding!(:profile)
420
+ redirect_to @profile
421
+ else
422
+ render :new, status: :unprocessable_entity
423
+ end
424
+ end
425
+ end
426
+ ```
427
+
428
+ `advance_onboarding!` is a no-op unless the named step is the user's
429
+ *current* onboarding step, so the same action behaves normally when the
430
+ user edits their profile outside onboarding.
431
+
432
+ A `path:` step with neither `complete_if:` nor `skippable: true` can only
433
+ advance through an explicit `advance_onboarding!` call - the validator logs
434
+ a warning for such steps so a missing call doesn't silently trap users.
435
+
436
+ To keep the guided feel while users are on your own pages, render the
437
+ gem's progress banner from your layout. It shows current step, progress,
438
+ a "Continue setup" link, and a skip button (on skippable steps), and renders
439
+ nothing once onboarding is done, skipped, or not required:
440
+
441
+ ```erb
442
+ <body>
443
+ <%= render "rails_onboarding/shared/onboarding_banner" %>
444
+ <%= yield %>
445
+ </body>
446
+ ```
447
+
448
+ If you guard pages with `redirect_to onboarding_path if needs_onboarding?`,
449
+ that guard automatically stays off the current step's own page - the gem
450
+ recognizes it and lets the request through, so the redirect loop you'd
451
+ expect from `/onboarding` bouncing to the page and back can't happen.
452
+
453
+ ### Milestone Configuration
454
+
455
+ ```ruby
456
+ RailsOnboarding.configure do |config|
457
+ config.enable_milestones = true
458
+
459
+ config.milestones = [
460
+ {
461
+ key: :profile_complete,
462
+ title: 'Profile Master',
463
+ description: 'Completed your profile',
464
+ points: 100,
465
+ icon: '👤',
466
+ trigger: :profile_completed
467
+ },
468
+ {
469
+ key: :first_post,
470
+ title: 'Content Creator',
471
+ description: 'Created your first post',
472
+ points: 50,
473
+ icon: '📝',
474
+ trigger: :post_created
475
+ }
476
+ ]
477
+ end
478
+ ```
479
+
480
+ Each milestone is identified by `key:`, not `id:` — `milestone_by_key`,
481
+ `milestone_achieved?` and the achievement records all look it up by that
482
+ field. A milestone without `key:` fails configuration validation at boot with
483
+ `InvalidMilestoneError`, so a typo here surfaces immediately rather than
484
+ producing achievements nothing can find. `trigger:` is required as well;
485
+ `title`, `description`, `points` and `icon` are what the milestone views
486
+ display.
487
+
488
+ ### Analytics Configuration
489
+
490
+ ```ruby
491
+ RailsOnboarding.configure do |config|
492
+ config.enable_analytics = true
493
+
494
+ # How long to keep analytics rows (days)
495
+ config.analytics_retention_days = 90
496
+
497
+ # Inactivity after which a visit counts as a new session
498
+ config.analytics_session_timeout_minutes = 30
499
+ end
500
+ ```
501
+
502
+ Onboarding events (`onboarding_started`, `step_viewed`, `step_completed`,
503
+ `onboarding_completed`, `onboarding_skipped`, tooltip views and dismissals) are
504
+ recorded automatically while `enable_analytics` is on; there is no per-event
505
+ opt-in list.
506
+
507
+ ## Usage
508
+
509
+ ### Checking Onboarding Status
510
+
511
+ ```ruby
512
+ # In your controllers or views
513
+ if current_user.needs_onboarding?
514
+ redirect_to rails_onboarding.onboarding_path
515
+ end
516
+
517
+ # Check completion
518
+ current_user.onboarding_completed? # => true/false
519
+ current_user.onboarding_in_progress? # => true/false
520
+
521
+ # Get progress
522
+ current_user.onboarding_progress # => 75 (percentage)
523
+ ```
524
+
525
+ ### Managing Onboarding State
526
+
527
+ ```ruby
528
+ # Advance to next step
529
+ current_user.advance_step!
530
+
531
+ # Go back to previous step
532
+ current_user.go_back!
533
+
534
+ # Complete onboarding
535
+ current_user.complete_onboarding!
536
+
537
+ # Skip onboarding
538
+ current_user.skip_onboarding!
539
+
540
+ # Restart onboarding
541
+ current_user.restart_onboarding!
542
+ ```
543
+
544
+ ### Working with Tooltips
545
+
546
+ ```ruby
547
+ # Check if tooltip has been shown
548
+ current_user.tooltip_shown?('feature_dashboard') # => false
549
+
550
+ # Mark tooltip as shown
551
+ current_user.mark_tooltip_shown!('feature_dashboard')
552
+
553
+ # Reset all tooltips
554
+ current_user.reset_tooltips!
555
+ ```
556
+
557
+ There's no `tooltip_tag` helper - render the Stimulus controller directly,
558
+ gated by `show_feature_tooltip?`, with the copy in a `content` target (the
559
+ controller can't read your Ruby config, so the text has to be in the DOM)
560
+ and the dismiss URL pointed at the engine's real route:
561
+
562
+ ```erb
563
+ <% if current_user.show_feature_tooltip?("feature_reports") %>
564
+ <div data-controller="tooltip"
565
+ data-tooltip-feature-value="feature_reports"
566
+ data-tooltip-position-value="bottom"
567
+ data-tooltip-delay-value="1000"
568
+ data-tooltip-dismiss-url-value="<%= rails_onboarding.dismiss_tooltip_path(tooltip_id: "feature_reports") %>">
569
+ <button data-tooltip-target="trigger" class="btn">Reports</button>
570
+ <div data-tooltip-target="content" hidden>
571
+ Click here to view your reports
572
+ </div>
573
+ </div>
574
+ <% end %>
575
+ ```
576
+
577
+ Register `config.feature_tooltips["feature_reports"]` in your initializer
578
+ first (see [Configuration](#configuration) below) - `show_feature_tooltip?`
579
+ returns false for anything not configured there, even if the DOM above is
580
+ present.
581
+
582
+ ### Working with Milestones
583
+
584
+ ```ruby
585
+ # Check milestone achievement
586
+ current_user.milestone_achieved?(:profile_complete) # => false
587
+
588
+ # Record an achievement. Points come from the milestone's configuration -
589
+ # they are not passed in. Returns false if milestones are disabled, the key
590
+ # is unknown, or it was already achieved.
591
+ current_user.achieve_milestone!(:profile_complete) # => true
592
+
593
+ # Get user's points
594
+ current_user.total_milestone_points # => 150
595
+
596
+ # List achieved milestone keys
597
+ current_user.achieved_milestones # => ['first_login', 'profile_complete']
598
+
599
+ # Turn those keys into renderable hashes
600
+ current_user.achieved_milestones.filter_map { |key|
601
+ RailsOnboarding.configuration.milestone_by_key(key)
602
+ }
603
+ ```
604
+
605
+ #### Displaying milestones
606
+
607
+ The engine ships a milestones dashboard at `rails_onboarding.milestones_path`,
608
+ and two partials you render yourself when you want achievements to appear
609
+ inside your own pages. Both take a milestone hash in the shape
610
+ `config.milestones` uses — `key`, `title`, `description`, `points`, `icon`.
611
+
612
+ **A badge**, for listing achievements in a profile or sidebar. `achieved:`
613
+ decides whether it renders unlocked or with a lock overlay:
614
+
615
+ ```erb
616
+ <% RailsOnboarding.configuration.milestones.each do |milestone| %>
617
+ <%= render "rails_onboarding/shared/milestone_badge",
618
+ milestone: milestone,
619
+ achieved: current_user.milestone_achieved?(milestone[:key]) %>
620
+ <% end %>
621
+ ```
622
+
623
+ **A celebration**, a modal overlay that shows itself as soon as it connects,
624
+ fires confetti, and auto-dismisses after eight seconds. Render it only when
625
+ you have something to celebrate — it is not self-guarding the way the
626
+ onboarding banner is:
627
+
628
+ ```erb
629
+ <% if (milestone = current_user.recent_milestones(limit: 1).first) %>
630
+ <%= render "rails_onboarding/shared/milestone_celebration",
631
+ milestone: milestone %>
632
+ <% end %>
633
+ ```
634
+
635
+ Both partials need `rails_onboarding/milestones.css` loaded, and the
636
+ celebration additionally needs its Stimulus controller registered — without
637
+ it the overlay stays hidden, since it is rendered with `is-hidden` and the
638
+ controller is what reveals it. See the
639
+ [Asset Loading Guide](docs/ASSET_LOADING_GUIDE.md) for both.
640
+
641
+ Useful sources for the milestone hashes:
642
+
643
+ | Call | Returns |
644
+ |---|---|
645
+ | `RailsOnboarding.configuration.milestones` | every configured milestone |
646
+ | `RailsOnboarding.configuration.milestone_by_key(key)` | one milestone, or `nil` |
647
+ | `current_user.milestones_available` | configured milestones not yet achieved |
648
+ | `current_user.recent_milestones(limit: 5)` | most recently achieved, newest last |
649
+ | `current_user.achieved_milestones` | achieved **keys**, not hashes |
650
+ | `current_user.total_milestone_points` | integer |
651
+
652
+ Note that `achieved_milestones` returns keys rather than hashes; pass each
653
+ through `milestone_by_key` to get something the partials can render.
654
+
655
+ ### Flash Messages
656
+
657
+ The engine reports some conditions — a step that can't be skipped, a "back"
658
+ from the first step — by replacing a `flash-messages` element over Turbo.
659
+ That element only exists if you render the gem's flash partial, so without it
660
+ those messages are silently dropped. Render it once in your layout:
661
+
662
+ ```erb
663
+ <body>
664
+ <%= render "rails_onboarding/shared/flash" %>
665
+ <%= yield %>
666
+ </body>
667
+ ```
668
+
669
+ Rendered with no locals it displays `flash[:notice]` and `flash[:alert]`,
670
+ because `notice` and `alert` are the standard Rails view helpers. You can
671
+ also pass messages explicitly, which is what the engine's own Turbo responses
672
+ do:
673
+
674
+ ```erb
675
+ <%= render "rails_onboarding/shared/flash", alert: "This step cannot be skipped." %>
676
+ ```
677
+
678
+ | Local | Style | Notes |
679
+ |---|---|---|
680
+ | `notice:` | success (green, ✓) | falls back to `flash[:notice]` |
681
+ | `alert:` | warning (amber, ⚠) | falls back to `flash[:alert]` |
682
+ | `error:` | error (red, ✕) | explicit only — there is no `error` view helper |
683
+
684
+ Each message renders with `role="alert"` and its own dismiss button, which
685
+ needs no JavaScript from you. Styling comes from
686
+ `rails_onboarding/flash_messages.css`.
687
+
688
+ Two things worth knowing. The partial's root element carries
689
+ `id="flash-messages"`, and that is what the engine's Turbo Stream responses
690
+ replace — so keep the id if you copy the partial into your own app, or those
691
+ updates will stop landing. And unlike the onboarding banner, this partial is
692
+ not self-guarding: rendered with no flash set and no locals it emits an empty
693
+ container, which is harmless but not nothing, so place it where an empty
694
+ `div` won't disturb your layout.
695
+
696
+ ### Analytics and Reporting
697
+
698
+ ```ruby
699
+ # Get completion rate
700
+ RailsOnboarding::Analytics.completion_rate(30.days.ago..Time.current)
701
+ # => 0.75
702
+
703
+ # Get average completion time
704
+ RailsOnboarding::Analytics.average_completion_time
705
+ # => 320.5 (seconds)
706
+
707
+ # Get step funnel
708
+ RailsOnboarding::Analytics.step_funnel
709
+ # => { welcome: { started: 100, completed: 95 }, profile: { started: 95, completed: 80 }, ... }
710
+
711
+ # Get drop-off points
712
+ RailsOnboarding::Analytics.drop_off_points
713
+ # => [{ step: 'profile', drop_off_rate: 0.15 }, ...]
714
+ ```
715
+
716
+ Run analytics reports:
717
+
718
+ ```bash
719
+ # Daily summary
720
+ rails rails_onboarding:analytics:daily_summary
721
+
722
+ # Weekly summary
723
+ rails rails_onboarding:analytics:weekly_summary
724
+
725
+ # Export data
726
+ rails rails_onboarding:analytics:export[30]
727
+ ```
728
+
729
+ ## Advanced Features
730
+
731
+ ### A/B Testing
732
+
733
+ Test different onboarding flows:
734
+
735
+ ```ruby
736
+ # In your User model
737
+ include RailsOnboarding::AbTestable
738
+
739
+ # Assign variant
740
+ current_user.assign_ab_variant('flow_test', 'variant_b')
741
+
742
+ # Track conversion
743
+ current_user.track_ab_conversion('flow_test')
744
+
745
+ # Get results
746
+ RailsOnboarding::AbTest.results('flow_test')
747
+ ```
748
+
749
+ ### Personalization
750
+
751
+ Adapt onboarding based on user type:
752
+
753
+ ```ruby
754
+ # In your initializer
755
+ RailsOnboarding.configure do |config|
756
+ config.personalization_enabled = true
757
+
758
+ # Method called on the user to determine their type (default: :user_type)
759
+ config.user_type_method = :user_type
760
+
761
+ # Each flow is a plain array of steps, keyed by user type
762
+ config.personalized_flows = {
763
+ developer: [
764
+ { name: :setup_api, title: 'Setup API Keys' },
765
+ { name: :first_integration, title: 'First Integration' }
766
+ ],
767
+ marketer: [
768
+ { name: :create_campaign, title: 'Create Campaign' },
769
+ { name: :add_tracking, title: 'Add Tracking' }
770
+ ]
771
+ }
772
+ end
773
+ ```
774
+
775
+ Include `RailsOnboarding::Personalizable` in your User model, and the matching
776
+ flow is applied when onboarding starts:
777
+
778
+ ```ruby
779
+ current_user.update(user_type: 'developer')
780
+ current_user.personalized_steps # => the developer flow
781
+ ```
782
+
783
+ ### Progressive Disclosure
784
+
785
+ Reveal features over time:
786
+
787
+ ```ruby
788
+ # In your controller
789
+ class FeaturesController < ApplicationController
790
+ include RailsOnboarding::ProgressiveDisclosure
791
+
792
+ def advanced_reports
793
+ feature = ProgressiveFeature.find_by(feature_key: 'advanced_reports')
794
+
795
+ if feature_unlocked_for?(current_user, feature)
796
+ # Show feature
797
+ else
798
+ # Show teaser or locked state
799
+ end
800
+ end
801
+ end
802
+ ```
803
+
804
+ ### Multi-Tenant Support
805
+
806
+ Different onboarding per organization:
807
+
808
+ ```ruby
809
+ # Configure per-organization
810
+ RailsOnboarding::MultiTenant.configure_for_organization(org.id) do |config|
811
+ config.steps = org.custom_onboarding_steps
812
+ config.enable_tooltips = org.tooltips_enabled?
813
+ end
814
+
815
+ # Use in your app
816
+ config = RailsOnboarding::MultiTenant.configuration_for(current_user.organization_id)
817
+ ```
818
+
819
+ ### Interactive Tours
820
+
821
+ Create guided tours with spotlight effects:
822
+
823
+ ```erb
824
+ <div data-controller="tour"
825
+ data-tour-steps-value='<%= tour_steps.to_json %>'>
826
+
827
+ <button data-action="tour#start">Start Tour</button>
828
+
829
+ <div data-tour-target="dashboard">Dashboard</div>
830
+ <div data-tour-target="reports">Reports</div>
831
+ </div>
832
+ ```
833
+
834
+ ### Background Jobs
835
+
836
+ Queue onboarding emails and notifications:
837
+
838
+ ```ruby
839
+ # In your config
840
+ RailsOnboarding.configure do |config|
841
+ config.background_jobs_enabled = true
842
+ config.background_jobs_queue = :default
843
+ end
844
+ ```
845
+
846
+ `BackgroundJobs` is a concern. Include it where you want to queue work, and
847
+ configure the adapter per class if the ActiveJob default doesn't suit:
848
+
849
+ ```ruby
850
+ class OnboardingController < ApplicationController
851
+ include RailsOnboarding::BackgroundJobs
852
+
853
+ configure_background_jobs(adapter: :sidekiq, queue: :onboarding)
854
+
855
+ def complete
856
+ queue_onboarding_welcome_email(current_user)
857
+ queue_milestone_achievement(current_user, :first_login)
858
+ end
859
+ end
860
+ ```
861
+
862
+ ### Skip Logic
863
+
864
+ Conditionally skip steps based on user data:
865
+
866
+ Skip conditions live on the step itself, under `skip_if:`. It accepts a proc, a
867
+ predicate method name, or a hash:
868
+
869
+ ```ruby
870
+ RailsOnboarding.configure do |config|
871
+ config.steps = [
872
+ { name: :welcome, title: 'Welcome' },
873
+ { name: :profile, title: 'Setup Profile',
874
+ skip_if: ->(user) { user.profile_complete? } },
875
+ { name: :payment, title: 'Add Payment',
876
+ skip_if: :free_plan? },
877
+ { name: :team, title: 'Invite Team',
878
+ skip_if: { account_type: 'individual' } }
879
+ ]
880
+ end
881
+ ```
882
+
883
+ The hash form matches an attribute by default, and also understands
884
+ `has_attribute`, `missing_attribute`, `attribute_equals`,
885
+ `attribute_not_equals`, `has_role` and `custom`. Combine several with
886
+ `operator: :all` (the default), `:any`, or `:none`:
887
+
888
+ ```ruby
889
+ skip_if: {
890
+ operator: :any,
891
+ has_role: :admin,
892
+ missing_attribute: :company_name
893
+ }
894
+ ```
895
+
896
+ A condition that raises is logged and treated as false, so a broken predicate
897
+ never blocks the flow.
898
+
899
+ ### Error Recovery
900
+
901
+ Handle failures gracefully:
902
+
903
+ ```ruby
904
+ # Retry failed steps
905
+ current_user.retry_step!('profile')
906
+
907
+ # Track errors
908
+ RailsOnboarding::ErrorRecovery.log_error(user, step, error)
909
+
910
+ # Get error count
911
+ current_user.step_error_count('profile')
912
+ ```
913
+
914
+ ## Admin Dashboard
915
+
916
+ The engine ships a full admin dashboard - a stats overview, per-user onboarding management (view progress, reset, complete, or restart onboarding), a flow editor, and A/B test management. It's mounted automatically wherever you mounted the engine, at `/admin` under that path (`/onboarding/admin` if you used the default mount point from the installer).
917
+
918
+ It ships gated behind an authorization check you need to wire up - by default nothing has admin access.
919
+
920
+ ### 1. Add an `admin?` method to your `User` model
921
+
922
+ ```ruby
923
+ class User < ApplicationRecord
924
+ def admin?
925
+ role == "admin" # or however you track it
926
+ end
927
+ end
928
+ ```
929
+
930
+ With this in place and a `current_user` method available on your `ApplicationController` (e.g. from Devise), the default check - `current_user.present? && current_user.admin?` - is all you need.
931
+
932
+ ### 2. Or override the authentication method for custom logic
933
+
934
+ If you need something more specific than `current_user.admin?` (a different gate, a different method name for the current user, etc.), define this instead and it takes priority over the default check entirely:
935
+
936
+ ```ruby
937
+ class ApplicationController < ActionController::Base
938
+ def authenticate_rails_onboarding_admin!
939
+ redirect_to root_path, alert: "Not authorized" unless current_user&.admin?
940
+ end
941
+ end
942
+ ```
943
+
944
+ Leaving both `admin?` and `authenticate_rails_onboarding_admin!` undefined isn't a silent hole - visiting the dashboard raises a `NotImplementedError` that redirects with a message telling you to define one of them.
945
+
946
+ No extra asset tags are required for the dashboard itself: its layout (`app/views/layouts/rails_onboarding/admin.html.erb`) is self-contained with its own `<head>` and stylesheet/JS tags, separate from your app's layout.
947
+
948
+ ### 3. Customizing the user search (encrypted email columns)
949
+
950
+ The User Management search box matches on email and ID with plain SQL. If your
951
+ `User` model encrypts email, that SQL is comparing against ciphertext, so the
952
+ admin adapts automatically: deterministic encryption falls back to exact
953
+ address matching, and non-deterministic encryption skips email entirely. ID
954
+ search always works.
955
+
956
+ Partial matching over an encrypted column can't be done in SQL at all. Only
957
+ your app knows what it's willing to trade for it, so supply the strategy
958
+ yourself:
959
+
960
+ ```ruby
961
+ # config/initializers/rails_onboarding.rb
962
+ config.admin_user_search = lambda do |scope, term|
963
+ needle = term.to_s.downcase
964
+ ids = scope.select(:id, :email).filter_map do |user|
965
+ user.id if user.email.to_s.downcase.include?(needle)
966
+ end
967
+ scope.where(id: ids)
968
+ end
969
+ ```
970
+
971
+ That example decrypts in Ruby, which leaks nothing and needs no schema change,
972
+ but is O(n) per search — fine for hundreds or low thousands of users, not for
973
+ a large table. The alternative is a searchable index of your own (a trigram or
974
+ prefix blind index), which scales but leaks substantially more about the
975
+ addresses than deterministic encryption alone.
976
+
977
+ The lambda receives the scope already narrowed by the status and step filters,
978
+ plus the raw search term, and **must return a relation** — returning an array
979
+ silently drops sorting and pagination.
980
+
981
+ ## Pre-built Templates
982
+
983
+ Use ready-made onboarding flows:
984
+
985
+ ```ruby
986
+ # Apply a template
987
+ RailsOnboarding::Templates.apply('saas')
988
+
989
+ # Available templates:
990
+ # - 'saas' - SaaS application onboarding
991
+ # - 'ecommerce' - E-commerce store setup
992
+ # - 'marketplace' - Marketplace seller onboarding
993
+ # - 'community' - Community platform onboarding
994
+ # - 'education' - Educational platform onboarding
995
+ ```
996
+
997
+ ## API Reference
998
+
999
+ ### User Model Methods (Onboardable Concern)
1000
+
1001
+ #### Status Methods
1002
+ - `needs_onboarding?` - Returns true if user needs to complete onboarding
1003
+ - `onboarding_completed?` - Returns true if onboarding is complete
1004
+ - `onboarding_in_progress?` - Returns true if onboarding is in progress
1005
+ - `onboarding_skipped?` - Returns true if user skipped onboarding
1006
+
1007
+ #### Navigation Methods
1008
+ - `current_step_index` - Returns index of current step
1009
+ - `next_step` - Returns next step name
1010
+ - `previous_step` - Returns previous step name
1011
+ - `can_go_back?` - Returns true if can navigate backwards
1012
+ - `last_step?` - Returns true if on last step
1013
+
1014
+ #### Action Methods
1015
+ - `advance_step!` - Move to next step
1016
+ - `go_back!` - Move to previous step
1017
+ - `complete_onboarding!` - Mark onboarding as complete
1018
+ - `skip_onboarding!` - Skip onboarding
1019
+ - `restart_onboarding!` - Restart from beginning
1020
+
1021
+ #### Progress Methods
1022
+ - `onboarding_progress` - Returns completion percentage (0-100)
1023
+
1024
+ #### Tooltip Methods
1025
+ - `tooltip_shown?(tooltip_id)` - Check if tooltip was shown
1026
+ - `mark_tooltip_shown!(tooltip_id)` - Mark tooltip as shown
1027
+ - `reset_tooltips!` - Reset all tooltips
1028
+
1029
+ #### Milestone Methods
1030
+ - `milestone_achieved?(key)` - Check achievement status
1031
+ - `achieve_milestone!(key, session_id: nil)` - Record achievement; points are read from the milestone's configuration
1032
+ - `total_milestone_points` - Get total points
1033
+ - `achieved_milestones` - Get achieved milestone **keys** (not hashes)
1034
+ - `milestones_available` - Configured milestones not yet achieved, as hashes
1035
+ - `recent_milestones(limit: 5)` - Most recently achieved, as hashes, newest last
1036
+
1037
+ ### Controller Helpers
1038
+
1039
+ - `require_onboarding` - Before action to enforce onboarding
1040
+ - `skip_onboarding_requirement` - Skip requirement for specific actions
1041
+ - `user_needs_onboarding?` - Check if current user needs onboarding
1042
+ - `onboarding_path` - Get path to onboarding flow
1043
+
1044
+ ### Configuration Class
1045
+
1046
+ - `RailsOnboarding.configure { |config| ... }` - Configure the gem
1047
+ - `RailsOnboarding.configuration` - Access current configuration
1048
+ - `RailsOnboarding.reset_configuration!` - Reset to defaults
1049
+
1050
+ ### Analytics Class
1051
+
1052
+ - `Analytics.completion_rate(date_range)` - Get completion rate
1053
+ - `Analytics.average_completion_time` - Get average time
1054
+ - `Analytics.step_funnel` - Get step-by-step funnel data
1055
+ - `Analytics.drop_off_points` - Identify where users drop off
1056
+ - `Analytics.tooltip_engagement` - Get tooltip metrics
1057
+
1058
+ ## Testing
1059
+
1060
+ The gem includes comprehensive test coverage. Run tests with:
1061
+
1062
+ ```bash
1063
+ # Run all tests
1064
+ bundle exec rails test
1065
+
1066
+ # Run specific test file
1067
+ bundle exec rails test test/integration/navigation_test.rb
1068
+
1069
+ # Run with coverage
1070
+ COVERAGE=true bundle exec rails test
1071
+ ```
1072
+
1073
+ ### Testing in Your Application
1074
+
1075
+ Add helpers to your test suite:
1076
+
1077
+ ```ruby
1078
+ # test/test_helper.rb
1079
+ require 'rails_onboarding/test_helpers'
1080
+
1081
+ class ActiveSupport::TestCase
1082
+ include RailsOnboarding::TestHelpers
1083
+ end
1084
+ ```
1085
+
1086
+ Use in tests:
1087
+
1088
+ ```ruby
1089
+ class MyFeatureTest < ActionDispatch::IntegrationTest
1090
+ test "requires onboarding" do
1091
+ user = create_user_needing_onboarding
1092
+
1093
+ sign_in user
1094
+ get dashboard_path
1095
+
1096
+ assert_redirected_to onboarding_path
1097
+ end
1098
+
1099
+ test "allows access after onboarding" do
1100
+ user = create_user_with_completed_onboarding
1101
+
1102
+ sign_in user
1103
+ get dashboard_path
1104
+
1105
+ assert_response :success
1106
+ end
1107
+ end
1108
+ ```
1109
+
1110
+ ## Internationalization
1111
+
1112
+ The gem supports multiple languages:
1113
+
1114
+ ```yaml
1115
+ # config/locales/rails_onboarding.en.yml
1116
+ en:
1117
+ rails_onboarding:
1118
+ welcome:
1119
+ title: "Welcome!"
1120
+ description: "Let's get started"
1121
+ buttons:
1122
+ next: "Next"
1123
+ previous: "Back"
1124
+ skip: "Skip for now"
1125
+ complete: "Finish"
1126
+ ```
1127
+
1128
+ Set user locale:
1129
+
1130
+ ```ruby
1131
+ # Before onboarding
1132
+ current_user.update(locale: 'es')
1133
+ ```
1134
+
1135
+ ## Troubleshooting
1136
+
1137
+ ### Onboarding not triggering
1138
+
1139
+ Ensure you have:
1140
+ 1. Included the concern in your User model
1141
+ 2. Run the migrations
1142
+ 3. Added a `before_action` that redirects when `needs_onboarding?` is true (see [Protect Your Controllers](#3-protect-your-controllers) - the gem doesn't add this callback for you)
1143
+ 4. Configured at least one step
1144
+
1145
+ ### Assets not loading
1146
+
1147
+ The engine's CSS and JS aren't included in your layout automatically.
1148
+
1149
+ **On Sprockets**, `rails_onboarding/application.css` bundles all of the
1150
+ gem's stylesheets itself via internal `require` directives, so one tag is
1151
+ enough:
1152
+
1153
+ ```erb
1154
+ <%= stylesheet_link_tag "rails_onboarding/application" %>
1155
+ <%= javascript_include_tag "rails_onboarding/application", defer: true %>
1156
+ ```
1157
+
1158
+ You can alternatively link them via the manifest:
1159
+
1160
+ ```js
1161
+ // app/assets/config/manifest.js
1162
+ //= link rails_onboarding/application.css
1163
+ //= link rails_onboarding/application.js
1164
+ ```
1165
+
1166
+ **On Propshaft** (Rails 8's default, including with cssbundling-rails),
1167
+ there's no bundling directive processor - `application.css`'s internal
1168
+ `require` directives are inert comments, so it only contains its own base
1169
+ styles/tokens. Link every stylesheet individually or you'll silently be
1170
+ missing tooltips, tours, milestones, mobile responsiveness, and more:
1171
+
1172
+ ```erb
1173
+ <%= stylesheet_link_tag "rails_onboarding/application" %>
1174
+ <%= stylesheet_link_tag "rails_onboarding/tooltips" %>
1175
+ <%= stylesheet_link_tag "rails_onboarding/utilities" %>
1176
+ <%= stylesheet_link_tag "rails_onboarding/accessibility" %>
1177
+ <%= stylesheet_link_tag "rails_onboarding/milestones" %>
1178
+ <%= stylesheet_link_tag "rails_onboarding/tour" %>
1179
+ <%= stylesheet_link_tag "rails_onboarding/flash_messages" %>
1180
+ <%= stylesheet_link_tag "rails_onboarding/progressive_disclosure" %>
1181
+ <%= stylesheet_link_tag "rails_onboarding/admin" %>
1182
+ <%= stylesheet_link_tag "rails_onboarding/mobile" %>
1183
+ <%= javascript_include_tag "rails_onboarding/application", defer: true %>
1184
+ ```
1185
+
1186
+ Keep `application` first (it defines the `--onboarding-*` custom properties
1187
+ the rest use) and `mobile` last (its media-query overrides need to win
1188
+ cascade ties). Skip `admin` if you don't use the admin dashboard. This is
1189
+ unrelated to cssbundling-rails - these tags bypass your JS/CSS build
1190
+ pipeline and are served by Propshaft directly.
1191
+
1192
+ If you're on Importmap, the engine's `config/importmap.rb` pins are drawn
1193
+ into your app's importmap automatically - you don't need to pin anything
1194
+ yourself. You do still need to import and register the Stimulus controllers
1195
+ you use (see [Asset Loading Guide](docs/ASSET_LOADING_GUIDE.md)), since
1196
+ pinning a module only makes it loadable, not registered with Stimulus.
1197
+
1198
+ ### Gem styles bleeding into my app's pages
1199
+
1200
+ They shouldn't, and as of 0.4.0 they don't. Every rule in
1201
+ the gem's stylesheets is confined to markup the engine owns, scoped with
1202
+ `:where(.onboarding-container)` for engine pages or
1203
+ `:where(.onboarding-banner)` for the banner rendered on your own pages. It
1204
+ is safe to bundle the gem's CSS globally.
1205
+
1206
+ If you are on an older version and see symptoms like every link on your
1207
+ homepage suddenly underlined, tables and fieldsets restyled, or buttons
1208
+ forced to a 44px minimum, that's the pre-fix `accessibility.css` — it
1209
+ carried unscoped `a`, `table`, `fieldset`, `button` and `[role="dialog"]`
1210
+ selectors, and under Sprockets `*= require rails_onboarding/application`
1211
+ pulls in every gem stylesheet via `require_tree`. Upgrade, or drop the
1212
+ `require` from your host `application.css` and link the individual
1213
+ stylesheets you need.
1214
+
1215
+ Two utility classes are namespaced rather than scoped, because they are
1216
+ meant to be used outside the container: `.onboarding-sr-only` (screen-reader
1217
+ text) and `.onboarding-skip-link` (a skip link has to be the first focusable
1218
+ element in the document). If you used the older `.sr-only` / `.skip-link`
1219
+ names in your own markup, rename them.
1220
+
1221
+ The gem does define `--onboarding-*` custom properties on `:root`, which is
1222
+ intentional — that's the supported way to retheme it from your own
1223
+ stylesheet:
1224
+
1225
+ ```css
1226
+ :root {
1227
+ --onboarding-primary: #0f766e;
1228
+ --onboarding-primary-hover: #115e59;
1229
+ }
1230
+ ```
1231
+
1232
+ ### Do I need to load accessibility.css?
1233
+
1234
+ Yes — load it like any other gem stylesheet. It isn't gated on a user
1235
+ preference and there's no toggle to switch it on for people who need it.
1236
+ Most of it is unconditional baseline: focus indicators, 44px touch targets,
1237
+ screen-reader text, disabled-state contrast. Focus rings use `:focus-visible`,
1238
+ so browsers already paint them only for keyboard and assistive-technology
1239
+ users, not on mouse clicks. The parts that *are* preference-dependent —
1240
+ `prefers-contrast: high`, `prefers-reduced-motion: reduce`,
1241
+ `prefers-color-scheme: dark` — are gated by the browser off the user's OS
1242
+ setting, with nothing for your app to detect or configure.
1243
+
1244
+ ### Stimulus controllers not working
1245
+
1246
+ Ensure Stimulus is installed and configured:
1247
+
1248
+ ```bash
1249
+ bin/rails stimulus:manifest:update
1250
+ ```
1251
+
1252
+ ## Upgrading
1253
+
1254
+ See [MIGRATION_GUIDE.md](docs/MIGRATION_GUIDE.md) for version upgrade instructions.
1255
+
1256
+ ## Performance Considerations
1257
+
1258
+ ### Caching
1259
+
1260
+ Enable caching for better performance:
1261
+
1262
+ Caching is opt-in at the call site rather than through configuration - include
1263
+ `RailsOnboarding::Caching` and use the cached readers, each taking its own
1264
+ `ttl:` in seconds:
1265
+
1266
+ ```ruby
1267
+ # config/environments/production.rb
1268
+ config.cache_store = :redis_cache_store
1269
+ ```
1270
+
1271
+ ```ruby
1272
+ class User < ApplicationRecord
1273
+ include RailsOnboarding::Caching
1274
+ end
1275
+
1276
+ User.cached_steps(ttl: 1.hour) # configuration lookups
1277
+ current_user.cached_onboarding_progress # per-user, defaults to 5 minutes
1278
+
1279
+ # Call after changing configuration
1280
+ User.clear_config_cache
1281
+ ```
1282
+
1283
+ ### Database Indexes
1284
+
1285
+ Add indexes for frequently queried fields:
1286
+
1287
+ ```ruby
1288
+ add_index :users, :onboarding_completed
1289
+ add_index :users, :onboarding_current_step
1290
+ add_index :analytics_events, [:user_id, :event_type]
1291
+ add_index :analytics_events, :created_at
1292
+ ```
1293
+
1294
+ ## Security
1295
+
1296
+ The gem follows Rails security best practices:
1297
+ - CSRF protection enabled
1298
+ - SQL injection prevention
1299
+ - XSS protection with content sanitization
1300
+ - Secure session handling
1301
+
1302
+ ## Browser Support
1303
+
1304
+ - Chrome/Edge (last 2 versions)
1305
+ - Firefox (last 2 versions)
1306
+ - Safari (last 2 versions)
1307
+ - Mobile browsers (iOS Safari, Chrome Mobile)
1308
+
1309
+ ## Contributing
1310
+
1311
+ Contributions are welcome. In short:
1312
+
1313
+ 1. Fork the repository and branch from `master`
1314
+ 2. Write a test for your change
1315
+ 3. Check it with `bin/rails test` and `bin/rubocop`
1316
+ 4. Add a changelog entry if the change is user-visible
1317
+ 5. Open a pull request
1318
+
1319
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, how the test suite is laid
1320
+ out, and the conventions this project follows.
1321
+
1322
+ ## Documentation
1323
+
1324
+ - [Advanced Features Guide](docs/ADVANCED_FEATURES.md)
1325
+ - [Milestone System Guide](docs/MILESTONES_GUIDE.md)
1326
+ - [Analytics Guide](docs/ANALYTICS_GUIDE.md)
1327
+ - [Responsive Design Guide](docs/RESPONSIVE_DESIGN.md)
1328
+ - [Performance & Scalability Guide](docs/PERFORMANCE_GUIDE.md)
1329
+ - [API Documentation](docs/API_DOCUMENTATION.md)
1330
+ - [Migration Guide](docs/MIGRATION_GUIDE.md)
1331
+ - [ESBuild Setup Guide](docs/ESBUILD_SETUP.md)
1332
+ - [Configuration Validation Guide](docs/CONFIGURATION_VALIDATION.md)
1333
+
1334
+ ## Credits
1335
+
1336
+ Created and maintained by [David Lewis](https://github.com/bunnahabhain).
1337
+
1338
+ ## License
1339
+
1340
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1341
+
1342
+ ## Support
1343
+
1344
+ - 📫 Email: david@davidsfolly.com
1345
+ - 🐛 Issues: [GitHub Issues](https://github.com/bunnahabhain/rails_onboarding/issues)
1346
+ - 💬 Discussions: [GitHub Discussions](https://github.com/bunnahabhain/rails_onboarding/discussions)
1347
+ - 📖 Wiki: [GitHub Wiki](https://github.com/bunnahabhain/rails_onboarding/wiki)
1348
+
1349
+ ## Changelog
1350
+
1351
+ See [CHANGELOG.md](docs/CHANGELOG.md) for a list of changes.