rails_onboarding 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. metadata +303 -0
@@ -0,0 +1,16 @@
1
+ # Test-only controller for setting session in integration tests
2
+ module RailsOnboarding
3
+ class TestSessionsController < ApplicationController
4
+ skip_before_action :verify_authenticity_token, only: :create
5
+
6
+ def create
7
+ # Only allow in test environment
8
+ if Rails.env.test?
9
+ session[:user_id] = params[:user_id]
10
+ head :ok
11
+ else
12
+ head :forbidden
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,92 @@
1
+ module RailsOnboarding
2
+ class TooltipsController < ApplicationController
3
+ include RailsOnboarding::RateLimitable
4
+
5
+ before_action :authenticate_user!
6
+
7
+ # POST /tooltips/dismiss
8
+ def dismiss
9
+ tooltip_id = params[:tooltip_id]
10
+
11
+ if tooltip_id.blank?
12
+ render json: { success: false, message: "tooltip_id is required" }, status: :bad_request
13
+ return
14
+ end
15
+
16
+ # For dismiss, we mark as shown and track as dismissed
17
+ if current_user.respond_to?(:dismiss_tooltip)
18
+ current_user.dismiss_tooltip(tooltip_id)
19
+ render json: { success: true }
20
+ elsif current_user.respond_to?(:mark_tooltip_shown!)
21
+ # Fallback: manually mark and track
22
+ tooltips = current_user.feature_tooltips_shown || {}
23
+ tooltips[tooltip_id.to_s] = Time.current.iso8601
24
+ current_user.update!(feature_tooltips_shown: tooltips)
25
+
26
+ # Track dismissal event
27
+ if current_user.respond_to?(:track_tooltip_interaction!)
28
+ current_user.track_tooltip_interaction!(tooltip_id, "dismissed")
29
+ end
30
+ render json: { success: true }
31
+ else
32
+ render json: { success: false, message: "User does not support tooltips" }, status: :unprocessable_entity
33
+ end
34
+ end
35
+
36
+ # POST /tooltips/show
37
+ def show
38
+ tooltip_id = params[:tooltip_id]
39
+
40
+ if tooltip_id.present? && current_user.respond_to?(:mark_tooltip_shown!)
41
+ current_user.mark_tooltip_shown!(tooltip_id)
42
+ render json: { success: true }
43
+ else
44
+ render json: { success: false, message: "Invalid tooltip_id or user" }, status: :unprocessable_entity
45
+ end
46
+ end
47
+
48
+ # POST /tooltips/reset
49
+ def reset
50
+ if current_user.respond_to?(:reset_tooltips!)
51
+ current_user.reset_tooltips!
52
+ render json: { success: true }
53
+ else
54
+ render json: { success: false, message: "User does not support tooltips" }, status: :unprocessable_entity
55
+ end
56
+ end
57
+
58
+ # GET /tooltips/status
59
+ def status
60
+ tooltip_id = params[:tooltip_id]
61
+
62
+ if tooltip_id.present? && current_user.respond_to?(:show_feature_tooltip?)
63
+ shown = !current_user.show_feature_tooltip?(tooltip_id)
64
+ render json: { shown: shown, tooltip_id: tooltip_id }
65
+ else
66
+ render json: { shown: false, message: "Invalid tooltip_id or user" }, status: :unprocessable_entity
67
+ end
68
+ end
69
+
70
+ # Legacy action - kept for backward compatibility
71
+ def mark_shown
72
+ feature = params[:feature]
73
+
74
+ if feature.present? && current_user.respond_to?(:mark_tooltip_shown!)
75
+ current_user.mark_tooltip_shown!(feature)
76
+ render json: { status: "success", feature: feature }
77
+ else
78
+ render json: { status: "error", message: "Invalid feature or user" }, status: :unprocessable_entity
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def authenticate_user!
85
+ # This should be overridden by the host app
86
+ # or use the host app's authentication
87
+ unless defined?(current_user) && current_user
88
+ render json: { status: "error", message: "Authentication required" }, status: :unauthorized
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,4 @@
1
+ module RailsOnboarding
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,137 @@
1
+ module RailsOnboarding
2
+ # I18n Helper Module
3
+ # Provides convenient methods for internationalization
4
+ module I18nHelper
5
+ # Translate a key with the rails_onboarding scope
6
+ #
7
+ # @param key [String, Symbol] Translation key
8
+ # @param options [Hash] Translation options
9
+ # @return [String] Translated string
10
+ def t_onboarding(key, **options)
11
+ I18n.t("rails_onboarding.#{key}", **options)
12
+ end
13
+
14
+ # Translate navigation text
15
+ def t_nav(key, **options)
16
+ t_onboarding("navigation.#{key}", **options)
17
+ end
18
+
19
+ # Translate action text
20
+ def t_action(key, **options)
21
+ t_onboarding("actions.#{key}", **options)
22
+ end
23
+
24
+ # Translate message text
25
+ def t_message(key, **options)
26
+ t_onboarding("messages.#{key}", **options)
27
+ end
28
+
29
+ # Translate error text
30
+ def t_error(key, **options)
31
+ t_onboarding("errors.#{key}", **options)
32
+ end
33
+
34
+ # Translate common text
35
+ def t_common(key, **options)
36
+ t_onboarding("common.#{key}", **options)
37
+ end
38
+
39
+ # Translate progress text
40
+ def t_progress(key, **options)
41
+ t_onboarding("progress.#{key}", **options)
42
+ end
43
+
44
+ # Translate milestone text
45
+ def t_milestone(key, **options)
46
+ t_onboarding("milestones.#{key}", **options)
47
+ end
48
+
49
+ # Get localized step title
50
+ #
51
+ # @param step [Hash] Step configuration
52
+ # @return [String] Localized step title
53
+ def localized_step_title(step)
54
+ return step[:title] unless step[:title_key]
55
+
56
+ I18n.t(step[:title_key], default: step[:title])
57
+ end
58
+
59
+ # Get localized step description
60
+ #
61
+ # @param step [Hash] Step configuration
62
+ # @return [String] Localized step description
63
+ def localized_step_description(step)
64
+ return step[:description] unless step[:description_key]
65
+
66
+ I18n.t(step[:description_key], default: step[:description] || "")
67
+ end
68
+
69
+ # Get localized milestone title
70
+ #
71
+ # @param milestone [Hash] Milestone configuration
72
+ # @return [String] Localized milestone title
73
+ def localized_milestone_title(milestone)
74
+ return milestone[:title] unless milestone[:title_key]
75
+
76
+ I18n.t(milestone[:title_key], default: milestone[:title])
77
+ end
78
+
79
+ # Get localized milestone description
80
+ #
81
+ # @param milestone [Hash] Milestone configuration
82
+ # @return [String] Localized milestone description
83
+ def localized_milestone_description(milestone)
84
+ return milestone[:description] unless milestone[:description_key]
85
+
86
+ I18n.t(milestone[:description_key], default: milestone[:description] || "")
87
+ end
88
+
89
+ # Get available locales for onboarding
90
+ #
91
+ # @return [Array<Symbol>] Available locale codes
92
+ def onboarding_locales
93
+ I18n.available_locales.select do |locale|
94
+ I18n.exists?("rails_onboarding", locale: locale)
95
+ end
96
+ end
97
+
98
+ # Check if a locale is available
99
+ #
100
+ # @param locale [Symbol, String] Locale code
101
+ # @return [Boolean] True if locale is available
102
+ def locale_available?(locale)
103
+ I18n.available_locales.include?(locale.to_sym) &&
104
+ I18n.exists?("rails_onboarding", locale: locale)
105
+ end
106
+
107
+ # Get user's preferred locale
108
+ #
109
+ # @param user [User] User object
110
+ # @return [Symbol] Locale code
111
+ def user_locale(user)
112
+ return I18n.default_locale unless user
113
+
114
+ # Try to get locale from user
115
+ user_locale = if user.respond_to?(:locale)
116
+ user.locale
117
+ elsif user.respond_to?(:language)
118
+ user.language
119
+ elsif user.respond_to?(:preferred_language)
120
+ user.preferred_language
121
+ end
122
+
123
+ return I18n.default_locale unless user_locale
124
+
125
+ locale_sym = user_locale.to_sym
126
+ locale_available?(locale_sym) ? locale_sym : I18n.default_locale
127
+ end
128
+
129
+ # Execute block with user's locale
130
+ #
131
+ # @param user [User] User object
132
+ # @yield Block to execute with user's locale
133
+ def with_user_locale(user, &block)
134
+ I18n.with_locale(user_locale(user), &block)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Helper methods for personalization features in views
5
+ module PersonalizationHelper
6
+ # Render content only for specific user types
7
+ #
8
+ # @param user_types [Array<Symbol>, Symbol] User type(s) to show content for
9
+ # @param user [Object] User object (defaults to current_user)
10
+ # @yield Block to render if user type matches
11
+ #
12
+ # @example
13
+ # <%= for_user_types(:business, :enterprise) do %>
14
+ # <p>This is only shown to business and enterprise users</p>
15
+ # <% end %>
16
+ def for_user_types(*user_types, user: nil)
17
+ user ||= current_user if respond_to?(:current_user)
18
+ return unless user
19
+
20
+ return unless user.respond_to?(:personalization_type)
21
+
22
+ user_type = user.personalization_type
23
+ return unless user_types.map(&:to_sym).include?(user_type.to_sym)
24
+
25
+ yield if block_given?
26
+ end
27
+
28
+ # Render different content based on user type
29
+ #
30
+ # @param user [Object] User object (defaults to current_user)
31
+ # @yield [user_type] Block that receives the user type
32
+ #
33
+ # @example
34
+ # <%= personalized_content do |type| %>
35
+ # <% if type == :business %>
36
+ # <p>Welcome, business user!</p>
37
+ # <% else %>
38
+ # <p>Welcome!</p>
39
+ # <% end %>
40
+ # <% end %>
41
+ def personalized_content(user: nil, &block)
42
+ user ||= current_user if respond_to?(:current_user)
43
+ return unless user
44
+
45
+ return unless user.respond_to?(:personalization_type)
46
+
47
+ user_type = user.personalization_type
48
+ yield(user_type) if block_given?
49
+ end
50
+
51
+ # Get a personalized message based on user type
52
+ #
53
+ # @param messages [Hash] Hash of user_type => message
54
+ # @param default [String] Default message if user type not found
55
+ # @param user [Object] User object (defaults to current_user)
56
+ # @return [String] The personalized message
57
+ #
58
+ # @example
59
+ # <%= personalized_message(
60
+ # { business: "Welcome to your business dashboard",
61
+ # individual: "Welcome to your personal dashboard" },
62
+ # default: "Welcome"
63
+ # ) %>
64
+ def personalized_message(messages, default: "Welcome", user: nil)
65
+ user ||= current_user if respond_to?(:current_user)
66
+ return default unless user
67
+
68
+ return default unless user.respond_to?(:personalization_type)
69
+
70
+ user_type = user.personalization_type
71
+ messages[user_type.to_sym] || default
72
+ end
73
+
74
+ # Get personalized steps for the current user
75
+ #
76
+ # @param user [Object] User object (defaults to current_user)
77
+ # @return [Array<Hash>] Array of step configurations
78
+ def personalized_steps(user: nil)
79
+ user ||= current_user if respond_to?(:current_user)
80
+ return RailsOnboarding.configuration.steps unless user
81
+
82
+ return RailsOnboarding.configuration.steps unless user.respond_to?(:personalized_steps)
83
+
84
+ user.personalized_steps
85
+ end
86
+
87
+ # Render a personalized progress indicator
88
+ #
89
+ # @param user [Object] User object (defaults to current_user)
90
+ # @return [String] HTML for progress indicator
91
+ def personalized_progress_indicator(user: nil)
92
+ user ||= current_user if respond_to?(:current_user)
93
+ return "" unless user
94
+
95
+ return "" unless user.respond_to?(:personalized_progress_percentage)
96
+
97
+ percentage = user.personalized_progress_percentage
98
+ total_steps = user.respond_to?(:personalized_total_steps) ? user.personalized_total_steps : 0
99
+
100
+ content_tag :div, class: "personalized-progress" do
101
+ concat(content_tag(:div, class: "progress-bar") do
102
+ content_tag(:div, "", class: "progress-fill", style: "width: #{percentage}%")
103
+ end)
104
+ concat(content_tag(:div, "#{percentage}% complete (Step #{user.personalized_step_index(user.onboarding_current_step) + 1} of #{total_steps})", class: "progress-text"))
105
+ end
106
+ end
107
+
108
+ # Check if a feature should be shown based on personalization
109
+ #
110
+ # @param feature_name [Symbol, String] Feature name
111
+ # @param user [Object] User object (defaults to current_user)
112
+ # @return [Boolean] True if feature should be shown
113
+ def show_personalized_feature?(feature_name, user: nil)
114
+ user ||= current_user if respond_to?(:current_user)
115
+ return true unless user
116
+
117
+ return true unless user.respond_to?(:show_personalized_feature?)
118
+
119
+ user.show_personalized_feature?(feature_name)
120
+ end
121
+
122
+ # Get personalized CTA (Call To Action) text
123
+ #
124
+ # @param action [Symbol, String] Action type (:next, :complete, :skip, etc.)
125
+ # @param user [Object] User object (defaults to current_user)
126
+ # @return [String] Personalized CTA text
127
+ def personalized_cta(action, user: nil)
128
+ user ||= current_user if respond_to?(:current_user)
129
+
130
+ user_type = user&.respond_to?(:personalization_type) ? user.personalization_type : nil
131
+
132
+ cta_map = {
133
+ next: {
134
+ business: "Continue to Business Setup",
135
+ enterprise: "Proceed to Enterprise Configuration",
136
+ individual: "Next Step",
137
+ default: "Continue"
138
+ },
139
+ complete: {
140
+ business: "Launch Your Business Account",
141
+ enterprise: "Activate Enterprise Features",
142
+ individual: "Get Started",
143
+ default: "Complete Setup"
144
+ },
145
+ skip: {
146
+ business: "Skip for Now (You can complete this later in Settings)",
147
+ enterprise: "Configure Later",
148
+ individual: "Skip This Step",
149
+ default: "Skip"
150
+ }
151
+ }
152
+
153
+ action_map = cta_map[action.to_sym] || {}
154
+ action_map[user_type&.to_sym] || action_map[:default] || action.to_s.titleize
155
+ end
156
+
157
+ # Render personalized recommendations
158
+ #
159
+ # @param user [Object] User object (defaults to current_user)
160
+ # @return [String] HTML for recommendations
161
+ def personalized_recommendations(user: nil)
162
+ user ||= current_user if respond_to?(:current_user)
163
+ return "" unless user
164
+
165
+ return "" unless user.respond_to?(:personalized_recommendations)
166
+
167
+ recommendations = user.personalized_recommendations
168
+ return "" if recommendations.empty?
169
+
170
+ content_tag :div, class: "personalized-recommendations" do
171
+ concat(content_tag(:h3, "Recommended for You"))
172
+ concat(content_tag(:div, class: "recommendations-list") do
173
+ recommendations.each do |rec|
174
+ concat(content_tag(:div, class: "recommendation-card") do
175
+ concat(content_tag(:h4, rec[:title]))
176
+ concat(content_tag(:p, rec[:description]))
177
+ concat(link_to(rec[:cta] || "Get Started", rec[:url], class: "btn btn-primary")) if rec[:url]
178
+ end)
179
+ end
180
+ end)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsOnboarding
4
+ # Helper methods for working with onboarding templates
5
+ module TemplatesHelper
6
+ # Get all available templates
7
+ #
8
+ # @return [Hash] Hash of template_key => template_config
9
+ def available_templates
10
+ RailsOnboarding.configuration.onboarding_templates || {}
11
+ end
12
+
13
+ # Get a specific template
14
+ #
15
+ # @param template_key [Symbol, String] Template identifier
16
+ # @return [Hash, nil] Template configuration or nil
17
+ def get_template(template_key)
18
+ RailsOnboarding.configuration.template(template_key)
19
+ end
20
+
21
+ # Apply a template to configuration
22
+ #
23
+ # @param template_key [Symbol, String] Template identifier
24
+ # @return [Boolean] Success status
25
+ def apply_template(template_key)
26
+ RailsOnboarding.configuration.apply_template(template_key)
27
+ end
28
+
29
+ # Get recommended template based on context
30
+ #
31
+ # @param context [Hash] Context information (industry, user_count, etc.)
32
+ # @return [Symbol, nil] Recommended template key or nil
33
+ #
34
+ # @example
35
+ # recommended_template(industry: :ecommerce, user_count: 1)
36
+ # # => :ecommerce
37
+ def recommended_template(context = {})
38
+ templates = available_templates
39
+
40
+ # Recommend based on industry if provided
41
+ if context[:industry]
42
+ return context[:industry].to_sym if templates[context[:industry].to_sym]
43
+ end
44
+
45
+ # Recommend based on user type
46
+ if context[:user_type]
47
+ case context[:user_type].to_sym
48
+ when :student, :learner
49
+ return :education if templates[:education]
50
+ when :seller, :merchant
51
+ return :marketplace if templates[:marketplace]
52
+ when :business, :company
53
+ return :saas if templates[:saas]
54
+ end
55
+ end
56
+
57
+ # Recommend based on team size
58
+ if context[:team_size]
59
+ if context[:team_size] > 10
60
+ return :saas if templates[:saas]
61
+ elsif context[:team_size] == 1
62
+ return :community if templates[:community]
63
+ end
64
+ end
65
+
66
+ # Default to first available template
67
+ templates.keys.first
68
+ end
69
+
70
+ # Render template selection UI
71
+ #
72
+ # @param options [Hash] Display options
73
+ # @return [String] HTML for template selector
74
+ def template_selector(options = {})
75
+ templates = available_templates
76
+ current_template = options[:current] || detect_current_template
77
+
78
+ content_tag :div, class: "template-selector" do
79
+ concat(content_tag(:h3, options[:title] || "Choose a Template"))
80
+
81
+ concat(content_tag(:div, class: "template-options") do
82
+ templates.each do |key, template|
83
+ concat(template_option(key, template, key == current_template))
84
+ end
85
+ end)
86
+ end
87
+ end
88
+
89
+ # Render a single template option
90
+ #
91
+ # @param key [Symbol] Template key
92
+ # @param template [Hash] Template configuration
93
+ # @param selected [Boolean] Whether this template is selected
94
+ # @return [String] HTML for template option
95
+ def template_option(key, template, selected = false)
96
+ content_tag :div, class: "template-option #{selected ? 'selected' : ''}", data: { template: key } do
97
+ concat(content_tag(:div, class: "template-icon") do
98
+ template[:icon] || "📋"
99
+ end)
100
+ concat(content_tag(:div, class: "template-info") do
101
+ concat(content_tag(:h4, template[:name]))
102
+ concat(content_tag(:p, template[:description] || "#{key.to_s.titleize} onboarding"))
103
+ concat(content_tag(:span, "#{template[:steps]&.size || 0} steps", class: "step-count"))
104
+ end)
105
+ end
106
+ end
107
+
108
+ # Detect which template matches current configuration
109
+ #
110
+ # @return [Symbol, nil] Matched template key or nil
111
+ def detect_current_template
112
+ current_steps = RailsOnboarding.configuration.steps
113
+ return nil if current_steps.empty?
114
+
115
+ available_templates.find do |key, template|
116
+ template[:steps] == current_steps
117
+ end&.first
118
+ end
119
+
120
+ # Compare templates
121
+ #
122
+ # @param template_keys [Array<Symbol>] Template keys to compare
123
+ # @return [Hash] Comparison data
124
+ def compare_templates(*template_keys)
125
+ comparison = {}
126
+
127
+ template_keys.each do |key|
128
+ template = get_template(key)
129
+ next unless template
130
+
131
+ comparison[key] = {
132
+ name: template[:name],
133
+ total_steps: template[:steps]&.size || 0,
134
+ required_steps: template[:steps]&.count { |s| !s[:skippable] } || 0,
135
+ optional_steps: template[:steps]&.count { |s| s[:skippable] } || 0,
136
+ steps: template[:steps]
137
+ }
138
+ end
139
+
140
+ comparison
141
+ end
142
+
143
+ # Render template comparison table
144
+ #
145
+ # @param template_keys [Array<Symbol>] Templates to compare
146
+ # @return [String] HTML table comparing templates
147
+ def render_template_comparison(*template_keys)
148
+ comparison = compare_templates(*template_keys)
149
+ return "" if comparison.empty?
150
+
151
+ content_tag :table, class: "template-comparison" do
152
+ concat(content_tag(:thead) do
153
+ content_tag(:tr) do
154
+ concat(content_tag(:th, "Feature"))
155
+ comparison.each_key do |key|
156
+ concat(content_tag(:th, comparison[key][:name]))
157
+ end
158
+ end
159
+ end)
160
+
161
+ concat(content_tag(:tbody) do
162
+ # Total steps row
163
+ concat(content_tag(:tr) do
164
+ concat(content_tag(:td, "Total Steps"))
165
+ comparison.each_value do |data|
166
+ concat(content_tag(:td, data[:total_steps]))
167
+ end
168
+ end)
169
+
170
+ # Required steps row
171
+ concat(content_tag(:tr) do
172
+ concat(content_tag(:td, "Required Steps"))
173
+ comparison.each_value do |data|
174
+ concat(content_tag(:td, data[:required_steps]))
175
+ end
176
+ end)
177
+
178
+ # Optional steps row
179
+ concat(content_tag(:tr) do
180
+ concat(content_tag(:td, "Optional Steps"))
181
+ comparison.each_value do |data|
182
+ concat(content_tag(:td, data[:optional_steps]))
183
+ end
184
+ end)
185
+ end)
186
+ end
187
+ end
188
+
189
+ # Get template metadata
190
+ #
191
+ # @param template_key [Symbol, String] Template identifier
192
+ # @return [Hash] Metadata about the template
193
+ def template_metadata(template_key)
194
+ template = get_template(template_key)
195
+ return {} unless template
196
+
197
+ {
198
+ name: template[:name],
199
+ total_steps: template[:steps]&.size || 0,
200
+ estimated_time: estimate_template_time(template),
201
+ difficulty: template_difficulty(template),
202
+ categories: template_categories(template)
203
+ }
204
+ end
205
+
206
+ # Estimate completion time for a template
207
+ #
208
+ # @param template [Hash] Template configuration
209
+ # @return [String] Estimated time (e.g., "5-10 minutes")
210
+ def estimate_template_time(template)
211
+ steps_count = template[:steps]&.size || 0
212
+ min_time = steps_count * 1 # 1 minute per step minimum
213
+ max_time = steps_count * 3 # 3 minutes per step maximum
214
+
215
+ "#{min_time}-#{max_time} minutes"
216
+ end
217
+
218
+ # Determine template difficulty
219
+ #
220
+ # @param template [Hash] Template configuration
221
+ # @return [Symbol] :easy, :medium, or :hard
222
+ def template_difficulty(template)
223
+ total_steps = template[:steps]&.size || 0
224
+ required_steps = template[:steps]&.count { |s| !s[:skippable] } || 0
225
+
226
+ if total_steps <= 3 && required_steps <= 2
227
+ :easy
228
+ elsif total_steps <= 5 && required_steps <= 3
229
+ :medium
230
+ else
231
+ :hard
232
+ end
233
+ end
234
+
235
+ # Get categories/tags for a template
236
+ #
237
+ # @param template [Hash] Template configuration
238
+ # @return [Array<Symbol>] Categories
239
+ def template_categories(template)
240
+ categories = []
241
+
242
+ step_names = template[:steps]&.map { |s| s[:name].to_s } || []
243
+
244
+ categories << :profile if step_names.any? { |n| n.include?("profile") }
245
+ categories << :team if step_names.any? { |n| n.include?("team") || n.include?("invite") }
246
+ categories << :payment if step_names.any? { |n| n.include?("payment") || n.include?("billing") }
247
+ categories << :content if step_names.any? { |n| n.include?("post") || n.include?("product") }
248
+ categories << :social if step_names.any? { |n| n.include?("connect") || n.include?("friends") }
249
+
250
+ categories
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,4 @@
1
+ module RailsOnboarding
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RailsOnboarding
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end