rails_onboarding 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- metadata +303 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module RailsOnboarding
|
|
6
|
+
# CDN Support module for serving assets from a CDN
|
|
7
|
+
#
|
|
8
|
+
# This module provides helpers and configuration for serving
|
|
9
|
+
# onboarding assets (JavaScript, CSS, images) from a CDN
|
|
10
|
+
# to improve performance through better caching and geographic distribution
|
|
11
|
+
module CdnSupport
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
# Get the CDN host URL
|
|
15
|
+
#
|
|
16
|
+
# @return [String, nil] CDN host URL or nil if not configured
|
|
17
|
+
def self.cdn_host
|
|
18
|
+
@cdn_host ||= begin
|
|
19
|
+
# Check Rails configuration first
|
|
20
|
+
if defined?(Rails.application) && Rails.application.config.action_controller.asset_host
|
|
21
|
+
Rails.application.config.action_controller.asset_host
|
|
22
|
+
else
|
|
23
|
+
ENV["RAILS_ONBOARDING_CDN_HOST"]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Set the CDN host URL
|
|
29
|
+
#
|
|
30
|
+
# @param host [String] CDN host URL
|
|
31
|
+
def self.cdn_host=(host)
|
|
32
|
+
@cdn_host = host
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check if CDN is enabled
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def self.cdn_enabled?
|
|
39
|
+
cdn_host.present? && !Rails.env.development?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get asset URL with CDN support
|
|
43
|
+
#
|
|
44
|
+
# @param asset_path [String] The asset path (e.g., 'rails_onboarding/application.css')
|
|
45
|
+
# @param asset_type [Symbol] Asset type (:stylesheet, :javascript, :image)
|
|
46
|
+
# @return [String] Full URL to the asset
|
|
47
|
+
def self.cdn_asset_url(asset_path, asset_type: :stylesheet)
|
|
48
|
+
if cdn_enabled?
|
|
49
|
+
case asset_type
|
|
50
|
+
when :stylesheet
|
|
51
|
+
"#{cdn_host}/assets/#{asset_path}"
|
|
52
|
+
when :javascript
|
|
53
|
+
"#{cdn_host}/assets/#{asset_path}"
|
|
54
|
+
when :image
|
|
55
|
+
"#{cdn_host}/assets/#{asset_path}"
|
|
56
|
+
else
|
|
57
|
+
"#{cdn_host}/assets/#{asset_path}"
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
# Fall back to local asset path
|
|
61
|
+
"/assets/#{asset_path}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Preload critical onboarding assets
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<Hash>] Array of asset preload hints
|
|
68
|
+
def self.preload_assets
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
href: cdn_asset_url("rails_onboarding/application.css", asset_type: :stylesheet),
|
|
72
|
+
as: "style",
|
|
73
|
+
type: "text/css"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
href: cdn_asset_url("rails_onboarding/application.js", asset_type: :javascript),
|
|
77
|
+
as: "script",
|
|
78
|
+
type: "text/javascript"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generate cache-busting URL with versioning
|
|
84
|
+
#
|
|
85
|
+
# @param asset_path [String] The asset path
|
|
86
|
+
# @param version [String] Asset version (defaults to gem version)
|
|
87
|
+
# @param asset_type [Symbol] Asset type (:stylesheet, :javascript, :image)
|
|
88
|
+
# @return [String] URL with version parameter
|
|
89
|
+
def self.versioned_asset_url(asset_path, version: nil, asset_type: :stylesheet)
|
|
90
|
+
version ||= RailsOnboarding::VERSION
|
|
91
|
+
url = cdn_asset_url(asset_path, asset_type: asset_type)
|
|
92
|
+
"#{url}?v=#{version}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Make these methods available as class methods when included
|
|
96
|
+
class_methods do
|
|
97
|
+
delegate :cdn_host, :cdn_host=, :cdn_enabled?, :cdn_asset_url,
|
|
98
|
+
:preload_assets, :versioned_asset_url, to: CdnSupport
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
module_function
|
|
102
|
+
|
|
103
|
+
# Helper method for view templates
|
|
104
|
+
# Generates link tags with CDN support and preload hints
|
|
105
|
+
#
|
|
106
|
+
# @param asset_path [String] Asset path
|
|
107
|
+
# @param options [Hash] Additional options
|
|
108
|
+
# @return [String] HTML link tag
|
|
109
|
+
def stylesheet_link_tag_with_cdn(asset_path, **options)
|
|
110
|
+
url = CdnSupport.versioned_asset_url(asset_path, asset_type: :stylesheet)
|
|
111
|
+
|
|
112
|
+
preload = options.delete(:preload)
|
|
113
|
+
crossorigin = options.delete(:crossorigin)
|
|
114
|
+
|
|
115
|
+
tag_options = {
|
|
116
|
+
rel: "stylesheet",
|
|
117
|
+
href: url
|
|
118
|
+
}.merge(options)
|
|
119
|
+
|
|
120
|
+
tag_options[:crossorigin] = crossorigin if crossorigin
|
|
121
|
+
|
|
122
|
+
tags = [ "<link #{cdn_tag_attributes(tag_options)} />" ]
|
|
123
|
+
|
|
124
|
+
if preload
|
|
125
|
+
tags.unshift("<link #{cdn_tag_attributes(rel: 'preload', href: url, as: 'style')} />")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
tags.join("\n").html_safe
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Helper method for JavaScript tags with CDN support
|
|
132
|
+
#
|
|
133
|
+
# @param asset_path [String] Asset path
|
|
134
|
+
# @param options [Hash] Additional options
|
|
135
|
+
# @return [String] HTML script tag
|
|
136
|
+
def javascript_include_tag_with_cdn(asset_path, **options)
|
|
137
|
+
url = CdnSupport.versioned_asset_url(asset_path, asset_type: :javascript)
|
|
138
|
+
|
|
139
|
+
preload = options.delete(:preload)
|
|
140
|
+
defer = options.delete(:defer) { true } # Default to defer
|
|
141
|
+
async = options.delete(:async)
|
|
142
|
+
|
|
143
|
+
tag_options = {
|
|
144
|
+
src: url,
|
|
145
|
+
type: "text/javascript"
|
|
146
|
+
}.merge(options)
|
|
147
|
+
|
|
148
|
+
tag_options[:defer] = "defer" if defer && !async
|
|
149
|
+
tag_options[:async] = "async" if async
|
|
150
|
+
|
|
151
|
+
tags = [ "<script #{cdn_tag_attributes(tag_options)}></script>" ]
|
|
152
|
+
|
|
153
|
+
if preload
|
|
154
|
+
tags.unshift("<link #{cdn_tag_attributes(rel: 'preload', href: url, as: 'script')} />")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
tags.join("\n").html_safe
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Generate resource hints for DNS prefetch and preconnect
|
|
161
|
+
#
|
|
162
|
+
# @return [String] HTML meta tags for resource hints
|
|
163
|
+
def cdn_resource_hints
|
|
164
|
+
return "" unless CdnSupport.cdn_enabled?
|
|
165
|
+
|
|
166
|
+
cdn_host = ERB::Util.html_escape(CdnSupport.cdn_host)
|
|
167
|
+
<<~HTML.html_safe
|
|
168
|
+
<!-- DNS Prefetch and Preconnect for CDN -->
|
|
169
|
+
<link rel="dns-prefetch" href="#{cdn_host}" />
|
|
170
|
+
<link rel="preconnect" href="#{cdn_host}" crossorigin />
|
|
171
|
+
HTML
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Render tag attributes with every value HTML-escaped, so a CDN host or
|
|
175
|
+
# asset path containing a quote or angle bracket can't break out of the
|
|
176
|
+
# attribute it's placed in. Used in place of ActionView's tag helpers
|
|
177
|
+
# because these methods are also called as CdnSupport module functions,
|
|
178
|
+
# outside any view context where `tag`/`content_tag` would be available.
|
|
179
|
+
def cdn_tag_attributes(attributes)
|
|
180
|
+
attributes.map { |key, value| "#{key}=\"#{ERB::Util.html_escape(value)}\"" }.join(" ")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Configure CDN headers for optimal caching
|
|
184
|
+
#
|
|
185
|
+
# @param max_age [Integer] Cache max age in seconds (default: 1 year)
|
|
186
|
+
# @return [Hash] Headers for CDN caching
|
|
187
|
+
def cdn_cache_headers(max_age: 31_536_000)
|
|
188
|
+
{
|
|
189
|
+
"Cache-Control" => "public, max-age=#{max_age}, immutable",
|
|
190
|
+
"Expires" => max_age.seconds.from_now.httpdate,
|
|
191
|
+
"Vary" => "Accept-Encoding"
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Extend ActionView helpers if in Rails context
|
|
198
|
+
if defined?(ActionView::Base)
|
|
199
|
+
ActionView::Base.class_eval do
|
|
200
|
+
include RailsOnboarding::CdnSupport
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# A/B testing configuration.
|
|
4
|
+
module AbTesting
|
|
5
|
+
attr_accessor :enable_ab_testing, :ab_tests
|
|
6
|
+
|
|
7
|
+
# Get a specific A/B test configuration
|
|
8
|
+
#
|
|
9
|
+
# @param test_name [Symbol, String] The name of the test
|
|
10
|
+
# @return [Hash, nil] The test configuration or nil
|
|
11
|
+
def ab_test(test_name)
|
|
12
|
+
return nil unless enable_ab_testing
|
|
13
|
+
ab_tests[test_name.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def initialize_ab_testing
|
|
19
|
+
@enable_ab_testing = false
|
|
20
|
+
@ab_tests = {}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Analytics tracking configuration.
|
|
4
|
+
module Analytics
|
|
5
|
+
attr_accessor :enable_analytics, :analytics_session_timeout_minutes
|
|
6
|
+
attr_reader :analytics_data_retention_days, :analytics_retention_days
|
|
7
|
+
|
|
8
|
+
# Keep analytics_retention_days in sync with analytics_data_retention_days
|
|
9
|
+
def analytics_retention_days=(value)
|
|
10
|
+
@analytics_retention_days = value
|
|
11
|
+
@analytics_data_retention_days = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analytics_data_retention_days=(value)
|
|
15
|
+
@analytics_data_retention_days = value
|
|
16
|
+
@analytics_retention_days = value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def initialize_analytics
|
|
22
|
+
@enable_analytics = true
|
|
23
|
+
@analytics_data_retention_days = 365 # Keep analytics data for 1 year
|
|
24
|
+
@analytics_retention_days = 365 # Alias for analytics_data_retention_days
|
|
25
|
+
@analytics_session_timeout_minutes = 30 # Consider session ended after 30 minutes of inactivity
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Integration & compatibility options (Devise, Turbo, API mode,
|
|
4
|
+
# background jobs, mailer).
|
|
5
|
+
module Integrations
|
|
6
|
+
attr_accessor :devise_integration_enabled,
|
|
7
|
+
:redirect_unconfirmed_to_onboarding,
|
|
8
|
+
:turbo_streams_enabled,
|
|
9
|
+
:turbo_morphing_enabled,
|
|
10
|
+
:api_mode_enabled,
|
|
11
|
+
:api_authentication_method,
|
|
12
|
+
:background_jobs_enabled,
|
|
13
|
+
:background_jobs_queue,
|
|
14
|
+
:mailer_from
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def initialize_integrations
|
|
19
|
+
@devise_integration_enabled = true
|
|
20
|
+
@redirect_unconfirmed_to_onboarding = false
|
|
21
|
+
@turbo_streams_enabled = true
|
|
22
|
+
@turbo_morphing_enabled = false
|
|
23
|
+
@api_mode_enabled = false
|
|
24
|
+
@api_authentication_method = :token
|
|
25
|
+
@background_jobs_enabled = false
|
|
26
|
+
@background_jobs_queue = :default
|
|
27
|
+
@mailer_from = "noreply@example.com"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Milestone configuration, lookups, and trigger matching.
|
|
4
|
+
module Milestones
|
|
5
|
+
def enable_milestones
|
|
6
|
+
tenant_override(:enable_milestones, @enable_milestones)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_writer :enable_milestones
|
|
10
|
+
|
|
11
|
+
def milestones
|
|
12
|
+
tenant_override(:milestones, @milestones)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Override setter to clear cache when configuration changes
|
|
16
|
+
def milestones=(value)
|
|
17
|
+
clear_cache!
|
|
18
|
+
@milestones = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def milestone_by_key(key)
|
|
22
|
+
return nil if key.nil?
|
|
23
|
+
|
|
24
|
+
@milestone_by_key_cache ||= {}
|
|
25
|
+
@milestone_by_key_cache[key.to_sym] ||= milestones.find { |m| m[:key].to_sym == key.to_sym }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def milestones_for_trigger(trigger, conditions = {})
|
|
29
|
+
cache_key = [ trigger, conditions ].hash
|
|
30
|
+
@milestones_for_trigger_cache ||= {}
|
|
31
|
+
@milestones_for_trigger_cache[cache_key] ||= milestones.select do |milestone|
|
|
32
|
+
# Match on trigger
|
|
33
|
+
next false unless milestone[:trigger] == trigger.to_sym
|
|
34
|
+
|
|
35
|
+
# If no conditions are provided, match all milestones with this trigger
|
|
36
|
+
next true if conditions.empty?
|
|
37
|
+
|
|
38
|
+
# If milestone has no conditions, but we're providing conditions, don't match
|
|
39
|
+
next false if milestone[:conditions].nil?
|
|
40
|
+
|
|
41
|
+
# Both have conditions, check if they match
|
|
42
|
+
conditions_match?(milestone[:conditions], conditions)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def conditions_match?(milestone_conditions, trigger_conditions)
|
|
49
|
+
return true if milestone_conditions.nil?
|
|
50
|
+
|
|
51
|
+
milestone_conditions.all? do |key, value|
|
|
52
|
+
trigger_conditions[key] == value || trigger_conditions[key.to_s] == value ||
|
|
53
|
+
trigger_conditions[key.to_sym] == value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def initialize_milestones
|
|
58
|
+
@enable_milestones = false
|
|
59
|
+
|
|
60
|
+
# Default milestones - can be customized
|
|
61
|
+
@milestones = [
|
|
62
|
+
{
|
|
63
|
+
key: :welcome_completed,
|
|
64
|
+
title: "Welcome Aboard!",
|
|
65
|
+
description: "You completed the welcome step",
|
|
66
|
+
icon: "🎉",
|
|
67
|
+
points: 10,
|
|
68
|
+
trigger: :onboarding_step_completed,
|
|
69
|
+
conditions: { step: :welcome }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: :onboarding_completed,
|
|
73
|
+
title: "Onboarding Champion",
|
|
74
|
+
description: "You completed the entire onboarding flow",
|
|
75
|
+
icon: "🏆",
|
|
76
|
+
points: 50,
|
|
77
|
+
trigger: :onboarding_completed
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: :early_adopter,
|
|
81
|
+
title: "Early Adopter",
|
|
82
|
+
description: "You joined within the first hour",
|
|
83
|
+
icon: "⚡",
|
|
84
|
+
points: 100,
|
|
85
|
+
trigger: :custom,
|
|
86
|
+
conditions: { early_adopter: true }
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Per-user-type personalized onboarding flows.
|
|
4
|
+
module Personalization
|
|
5
|
+
attr_accessor :personalization_enabled, :user_type_method, :personalized_flows
|
|
6
|
+
|
|
7
|
+
# Get a personalized flow for a user type
|
|
8
|
+
#
|
|
9
|
+
# @param user_type [Symbol, String] The user type
|
|
10
|
+
# @return [Array, nil] The personalized steps or nil
|
|
11
|
+
def personalized_flow(user_type)
|
|
12
|
+
return nil unless personalization_enabled
|
|
13
|
+
personalized_flows[user_type.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def initialize_personalization
|
|
19
|
+
@personalization_enabled = false
|
|
20
|
+
@user_type_method = :user_type # Method to call on user to determine their type
|
|
21
|
+
@personalized_flows = {}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Progressive feature disclosure configuration.
|
|
4
|
+
module ProgressiveDisclosure
|
|
5
|
+
attr_accessor :progressive_disclosure_enabled, :progressive_features
|
|
6
|
+
|
|
7
|
+
# Check if a feature should be shown based on progressive disclosure
|
|
8
|
+
#
|
|
9
|
+
# @param feature_key [Symbol, String] The feature key
|
|
10
|
+
# @param user [Object] The user object
|
|
11
|
+
# @return [Boolean] True if feature should be shown
|
|
12
|
+
def show_progressive_feature?(feature_key, user)
|
|
13
|
+
return true unless progressive_disclosure_enabled
|
|
14
|
+
|
|
15
|
+
feature = progressive_features.find { |f| f[:key] == feature_key.to_sym }
|
|
16
|
+
return true unless feature
|
|
17
|
+
|
|
18
|
+
# Check if feature meets its reveal conditions
|
|
19
|
+
case feature[:reveal_condition]
|
|
20
|
+
when :time_based
|
|
21
|
+
user.created_at + feature[:delay].seconds <= Time.current
|
|
22
|
+
when :action_based
|
|
23
|
+
user.send(feature[:check_method]) if user.respond_to?(feature[:check_method])
|
|
24
|
+
when :step_based
|
|
25
|
+
step_index(user.onboarding_current_step) >= step_index(feature[:after_step])
|
|
26
|
+
else
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
rescue StandardError
|
|
30
|
+
true # Default to showing feature if check fails
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def initialize_progressive_disclosure
|
|
36
|
+
@progressive_disclosure_enabled = false
|
|
37
|
+
@progressive_features = []
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Rate limiting configuration (RailsOnboarding::RateLimitable) - opt-in.
|
|
4
|
+
module RateLimiting
|
|
5
|
+
attr_accessor :rate_limiting_enabled, :rate_limit_per_period, :rate_limit_period
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def initialize_rate_limiting
|
|
10
|
+
# Defaulting this on would start enforcing a request limit on every
|
|
11
|
+
# host app that upgrades without them ever having asked for it.
|
|
12
|
+
@rate_limiting_enabled = false
|
|
13
|
+
@rate_limit_per_period = 60 # Number of requests allowed per period
|
|
14
|
+
@rate_limit_period = 60 # Period in seconds (60 seconds = 1 minute)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Onboarding step configuration and lookups.
|
|
4
|
+
module Steps
|
|
5
|
+
attr_accessor :onboarding_required_for
|
|
6
|
+
|
|
7
|
+
# Reads the active RailsOnboarding::Flow from the admin Flow Editor when one
|
|
8
|
+
# exists, falling back to the statically-configured steps otherwise. This
|
|
9
|
+
# keeps "activate a flow" a real, shared, durable change instead of a
|
|
10
|
+
# per-process global mutation - every process re-checks the database
|
|
11
|
+
# instead of caching a value that could go stale the moment another
|
|
12
|
+
# process (or another admin) activates a different flow.
|
|
13
|
+
def steps
|
|
14
|
+
flow_steps = active_flow_steps
|
|
15
|
+
return hydrate_code_options(flow_steps) if flow_steps
|
|
16
|
+
|
|
17
|
+
tenant_override(:steps, @steps)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Override setter to clear cache when configuration changes
|
|
21
|
+
def steps=(value)
|
|
22
|
+
clear_cache!
|
|
23
|
+
@steps = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def total_steps
|
|
27
|
+
@total_steps ||= steps.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def step_by_name(name)
|
|
31
|
+
return nil if name.nil?
|
|
32
|
+
|
|
33
|
+
@step_by_name_cache ||= {}
|
|
34
|
+
@step_by_name_cache[name.to_sym] ||= steps.find do |s|
|
|
35
|
+
next false unless s.is_a?(Hash) && s[:name]
|
|
36
|
+
s[:name].to_sym == name.to_sym
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def step_index(name)
|
|
41
|
+
return nil if name.nil?
|
|
42
|
+
|
|
43
|
+
@step_index_cache ||= {}
|
|
44
|
+
@step_index_cache[name.to_sym] ||= steps.find_index do |s|
|
|
45
|
+
next false unless s.is_a?(Hash) && s[:name]
|
|
46
|
+
s[:name].to_sym == name.to_sym
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def active_flow_steps
|
|
53
|
+
return nil unless defined?(RailsOnboarding::Flow)
|
|
54
|
+
return nil unless RailsOnboarding::Flow.table_exists?
|
|
55
|
+
|
|
56
|
+
RailsOnboarding::Flow.active.first&.steps
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A flow persisted by the admin Flow Editor is stored as JSON, so any
|
|
62
|
+
# Proc-valued step option (:path, :complete_if, or a custom callable) was
|
|
63
|
+
# silently dropped on write - JSON can't serialize a Proc. Merge those
|
|
64
|
+
# code-only options back in from the statically-configured step of the
|
|
65
|
+
# same name, so activating a flow never disables a path-based step or its
|
|
66
|
+
# completion criteria. Presentation and ordering still come entirely from
|
|
67
|
+
# the flow; only behavior that can only live in code is re-hydrated.
|
|
68
|
+
def hydrate_code_options(flow_steps)
|
|
69
|
+
config_by_name = Array(@steps).each_with_object({}) do |step, index|
|
|
70
|
+
index[step[:name].to_sym] = step if step.is_a?(Hash) && step[:name]
|
|
71
|
+
end
|
|
72
|
+
return Array(flow_steps) if config_by_name.empty?
|
|
73
|
+
|
|
74
|
+
Array(flow_steps).map do |flow_step|
|
|
75
|
+
name = flow_step[:name] if flow_step.respond_to?(:[])
|
|
76
|
+
config_step = name && config_by_name[name.to_sym]
|
|
77
|
+
next flow_step unless config_step
|
|
78
|
+
|
|
79
|
+
code_options = config_step.select { |_key, value| value.is_a?(Proc) }
|
|
80
|
+
next flow_step if code_options.empty?
|
|
81
|
+
|
|
82
|
+
hydrated = flow_step.dup
|
|
83
|
+
code_options.each { |key, value| hydrated[key] = value }
|
|
84
|
+
hydrated
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def initialize_steps
|
|
89
|
+
@onboarding_required_for = :new_users # :new_users, :all_users, or a Proc
|
|
90
|
+
|
|
91
|
+
# Default steps - can be customized
|
|
92
|
+
@steps = [
|
|
93
|
+
{
|
|
94
|
+
name: :welcome,
|
|
95
|
+
title: "Welcome",
|
|
96
|
+
icon: "🎉",
|
|
97
|
+
skippable: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: :profile,
|
|
101
|
+
title: "Setup Profile",
|
|
102
|
+
icon: "👤",
|
|
103
|
+
skippable: false
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: :first_action,
|
|
107
|
+
title: "First Action",
|
|
108
|
+
icon: "🚀",
|
|
109
|
+
skippable: false
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: :explore,
|
|
113
|
+
title: "Explore Features",
|
|
114
|
+
icon: "🔍",
|
|
115
|
+
skippable: true
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module RailsOnboarding
|
|
2
|
+
class Configuration
|
|
3
|
+
# Pre-built onboarding step templates for common app types.
|
|
4
|
+
module Templates
|
|
5
|
+
attr_accessor :onboarding_templates
|
|
6
|
+
|
|
7
|
+
# Get an onboarding template by key
|
|
8
|
+
#
|
|
9
|
+
# @param template_key [Symbol, String] The template key
|
|
10
|
+
# @return [Hash, nil] The template configuration or nil
|
|
11
|
+
def template(template_key)
|
|
12
|
+
onboarding_templates[template_key.to_sym]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Apply a template to the current configuration
|
|
16
|
+
#
|
|
17
|
+
# @param template_key [Symbol, String] The template key to apply
|
|
18
|
+
# @return [Boolean] True if template was applied successfully
|
|
19
|
+
def apply_template(template_key)
|
|
20
|
+
template = onboarding_templates[template_key.to_sym]
|
|
21
|
+
return false unless template
|
|
22
|
+
|
|
23
|
+
@steps = template[:steps]
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def initialize_templates
|
|
30
|
+
@onboarding_templates = {
|
|
31
|
+
saas: {
|
|
32
|
+
name: "SaaS Application",
|
|
33
|
+
steps: [
|
|
34
|
+
{ name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
|
|
35
|
+
{ name: :account_setup, title: "Account Setup", icon: "👤", skippable: false },
|
|
36
|
+
{ name: :team_invite, title: "Invite Team", icon: "👥", skippable: true },
|
|
37
|
+
{ name: :first_project, title: "Create Project", icon: "📁", skippable: false },
|
|
38
|
+
{ name: :integration, title: "Connect Tools", icon: "🔌", skippable: true }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
ecommerce: {
|
|
42
|
+
name: "E-commerce Platform",
|
|
43
|
+
steps: [
|
|
44
|
+
{ name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
|
|
45
|
+
{ name: :store_setup, title: "Setup Store", icon: "🏪", skippable: false },
|
|
46
|
+
{ name: :first_product, title: "Add Product", icon: "📦", skippable: false },
|
|
47
|
+
{ name: :payment_setup, title: "Payment Setup", icon: "💳", skippable: false },
|
|
48
|
+
{ name: :launch, title: "Launch Store", icon: "🚀", skippable: false }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
marketplace: {
|
|
52
|
+
name: "Marketplace",
|
|
53
|
+
steps: [
|
|
54
|
+
{ name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
|
|
55
|
+
{ name: :profile_setup, title: "Create Profile", icon: "👤", skippable: false },
|
|
56
|
+
{ name: :verification, title: "Verify Account", icon: "✅", skippable: false },
|
|
57
|
+
{ name: :first_listing, title: "Create Listing", icon: "📝", skippable: false },
|
|
58
|
+
{ name: :explore, title: "Explore", icon: "🔍", skippable: true }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
community: {
|
|
62
|
+
name: "Community Platform",
|
|
63
|
+
steps: [
|
|
64
|
+
{ name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
|
|
65
|
+
{ name: :profile, title: "Setup Profile", icon: "👤", skippable: false },
|
|
66
|
+
{ name: :interests, title: "Choose Interests", icon: "❤️", skippable: true },
|
|
67
|
+
{ name: :first_post, title: "Create Post", icon: "✍️", skippable: false },
|
|
68
|
+
{ name: :connect, title: "Connect", icon: "🤝", skippable: true }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
education: {
|
|
72
|
+
name: "Educational Platform",
|
|
73
|
+
steps: [
|
|
74
|
+
{ name: :welcome, title: "Welcome", icon: "🎉", skippable: true },
|
|
75
|
+
{ name: :student_setup, title: "Student Info", icon: "🎓", skippable: false },
|
|
76
|
+
{ name: :course_selection, title: "Choose Courses", icon: "📚", skippable: false },
|
|
77
|
+
{ name: :first_lesson, title: "First Lesson", icon: "📖", skippable: false },
|
|
78
|
+
{ name: :study_plan, title: "Study Plan", icon: "📅", skippable: true }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|