rails_onboarding 0.6.3 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54c87e18cdea35b440ea3d1f9dcd93984da008e6690bbf0dba255e8b1b803266
4
- data.tar.gz: 6675e0f3bf44029a6ee5d2b6c6d051edf877dd388e087b44925a716f51070a1b
3
+ metadata.gz: c0a6325415445d0fcf87f3513dcfda910866b9bbbf93b8edde3e2c904af7ee15
4
+ data.tar.gz: 4db74bd332e05a95ba79404499c314c4dbb251511d075ef1b56746559a96963b
5
5
  SHA512:
6
- metadata.gz: fd7a88c2058c5ea08bc5636990af96fd58775bfa98bf8f5a93849793564afe09bb51dcdf4983641dab499cd850213e6b24c2c867c44137b4ae30b7cc469697b0
7
- data.tar.gz: 75827eaa49ba15a510ae5212be0ba4f6ea67739b32e1da632c9e9fd36eaa452a721d635f839e01f77922755ed15742e9d31f91618dcca0485f4c012b4e6088d3
6
+ metadata.gz: 4a3f455ac868d10a4cc011f434f5e2f8ab4e7b72c9769ebbfccd7ad2a0af78af0dc132d23771bd899926a05075ab025e1c53e2d4dacd8df329220e43b55899e9
7
+ data.tar.gz: 5a6d4ab7899f89c143754b9c2c5d85efb44efaeb5e8a7c0f1a0df9dd009961cf39614f91085f4d51de8ccfb76eacf85d56a205c857b5d4298af984e4b82b14b0
data/README.md CHANGED
@@ -537,7 +537,11 @@ current_user.complete_onboarding!
537
537
  # Skip onboarding
538
538
  current_user.skip_onboarding!
539
539
 
540
- # Restart onboarding
540
+ # Reset onboarding state (clears milestones too; pass clear_milestones: false to keep them)
541
+ current_user.reset_onboarding!
542
+
543
+ # Restart onboarding - resets and walks the user through the steps again, even
544
+ # if their data already satisfies every :complete_if
541
545
  current_user.restart_onboarding!
542
546
  ```
543
547
 
@@ -1016,7 +1020,9 @@ RailsOnboarding::Templates.apply('saas')
1016
1020
  - `go_back!` - Move to previous step
1017
1021
  - `complete_onboarding!` - Mark onboarding as complete
1018
1022
  - `skip_onboarding!` - Skip onboarding
1019
- - `restart_onboarding!` - Restart from beginning
1023
+ - `reset_onboarding!(clear_milestones: true)` - Clear onboarding state
1024
+ - `restart_onboarding!` - Reset and re-walk the flow (replay mode)
1025
+ - `replaying_onboarding?` - Is the user re-walking the flow?
1020
1026
 
1021
1027
  #### Progress Methods
1022
1028
  - `onboarding_progress` - Returns completion percentage (0-100)
@@ -61,7 +61,19 @@ module RailsOnboarding
61
61
  @user = user_class.find(params[:id])
62
62
 
63
63
  @user.restart_onboarding!
64
- flash[:notice] = "Onboarding restarted for user #{@user.id}"
64
+
65
+ # Without the replay columns a restart can't stop :complete_if from
66
+ # auto-advancing an established user straight back to "completed",
67
+ # which looks like the button did nothing. Say so rather than claiming
68
+ # a restart that won't survive their next page load.
69
+ flash[:notice] = if !user_class.respond_to?(:onboarding_replay_supported?) ||
70
+ user_class.onboarding_replay_supported?
71
+ "Onboarding restarted for user #{@user.id}"
72
+ else
73
+ "Onboarding restarted for user #{@user.id}. Steps whose completion criteria " \
74
+ "are already met will be skipped - run the add_onboarding_replay_to_users " \
75
+ "migration to walk this user through them again."
76
+ end
65
77
  redirect_to admin_user_path(@user)
66
78
  rescue StandardError => e
67
79
  flash[:alert] = "Error restarting onboarding: #{e.message}"
@@ -55,6 +55,12 @@ module RailsOnboarding
55
55
  )
56
56
  end
57
57
 
58
+ # Record that the user has now been shown this step. That's what lets
59
+ # :complete_if advance them past it when they come back, and it is what
60
+ # makes a restart re-walk a flow the user's data already satisfies.
61
+ # No-op outside a replay.
62
+ mark_step_replayed(@current_step)
63
+
58
64
  # Steps that own a real page in the host app: route there instead of
59
65
  # rendering a gem template, so the existing controller/view do the work.
60
66
  if @current_step[:path]
@@ -232,6 +238,10 @@ module RailsOnboarding
232
238
  awarded_milestones = []
233
239
  RailsOnboarding.configuration.total_steps.times do
234
240
  break unless @current_step && onboarding_step_criteria_met?(@current_step)
241
+ # During a replay, a satisfied :complete_if is not enough on its own -
242
+ # the user has to have been shown the step again first, or a restart
243
+ # would auto-advance them straight back to "completed".
244
+ break unless step_replayed?(@current_step)
235
245
 
236
246
  if defined?(RailsOnboarding::MilestoneService)
237
247
  milestones = RailsOnboarding::MilestoneService.check_onboarding_step_milestones(
@@ -258,6 +268,21 @@ module RailsOnboarding
258
268
  set_step
259
269
  end
260
270
 
271
+ # Replay bookkeeping. Both tolerate a host User that predates replay
272
+ # support (or isn't Onboardable at all), falling back to the ordinary
273
+ # auto-advance behaviour.
274
+ def step_replayed?(step)
275
+ return true unless current_user.respond_to?(:onboarding_step_replayed?)
276
+
277
+ current_user.onboarding_step_replayed?(step[:name])
278
+ end
279
+
280
+ def mark_step_replayed(step)
281
+ return unless current_user.respond_to?(:mark_onboarding_step_replayed!)
282
+
283
+ current_user.mark_onboarding_step_replayed!(step[:name])
284
+ end
285
+
261
286
  # Redirect to the host-app page that owns the current step. Returns false
262
287
  # (without redirecting) when the configured path can't be resolved, so
263
288
  # show can fall back to rendering a gem template instead of stranding the
@@ -18,6 +18,8 @@ module RailsOnboarding
18
18
  # - milestones_achieved: text (serialized JSON array)
19
19
  # - milestone_points: integer
20
20
  # - last_milestone_at: datetime
21
+ # - onboarding_replay_started_at: datetime
22
+ # - onboarding_replay_steps: text (serialized JSON array)
21
23
 
22
24
  # Fix for Rails 8: Use the new serialize syntax
23
25
  if columns_hash["feature_tooltips_shown"]&.type == :text
@@ -28,6 +30,10 @@ module RailsOnboarding
28
30
  serialize :milestones_achieved, coder: JSON
29
31
  end
30
32
 
33
+ if columns_hash["onboarding_replay_steps"]&.type == :text
34
+ serialize :onboarding_replay_steps, coder: JSON
35
+ end
36
+
31
37
  # Association with analytics events (only if ActiveRecord is available)
32
38
  if respond_to?(:has_many)
33
39
  has_many :analytics_events,
@@ -43,6 +49,19 @@ module RailsOnboarding
43
49
  end
44
50
  end
45
51
 
52
+ class_methods do
53
+ # Replay mode needs two columns that predate no host app: installs from
54
+ # before it existed simply don't have them. Everything replay-related
55
+ # degrades to the old auto-advance behaviour when they're missing rather
56
+ # than raising on a column that isn't there.
57
+ def onboarding_replay_supported?
58
+ return false unless respond_to?(:column_names)
59
+
60
+ column_names.include?("onboarding_replay_started_at") &&
61
+ column_names.include?("onboarding_replay_steps")
62
+ end
63
+ end
64
+
46
65
  # Determines if the user needs to go through onboarding
47
66
  #
48
67
  # This method checks the configuration setting and user state to determine
@@ -163,9 +182,11 @@ module RailsOnboarding
163
182
 
164
183
  def complete_onboarding!(session_id: nil, completion_time: nil)
165
184
  persist_and_track!(
166
- onboarding_completed: true,
167
- onboarding_completed_at: Time.current,
168
- onboarding_current_step: nil
185
+ {
186
+ onboarding_completed: true,
187
+ onboarding_completed_at: Time.current,
188
+ onboarding_current_step: nil
189
+ }.merge(replay_reset_attributes)
169
190
  ) do
170
191
  AnalyticsEvent.track_onboarding_completed(
171
192
  user: self,
@@ -179,10 +200,12 @@ module RailsOnboarding
179
200
 
180
201
  def skip_onboarding!(session_id: nil)
181
202
  persist_and_track!(
182
- onboarding_completed: true,
183
- onboarding_completed_at: Time.current,
184
- onboarding_skipped: true,
185
- onboarding_current_step: nil
203
+ {
204
+ onboarding_completed: true,
205
+ onboarding_completed_at: Time.current,
206
+ onboarding_skipped: true,
207
+ onboarding_current_step: nil
208
+ }.merge(replay_reset_attributes)
186
209
  ) do
187
210
  AnalyticsEvent.track_onboarding_completed(
188
211
  user: self,
@@ -193,14 +216,35 @@ module RailsOnboarding
193
216
  end
194
217
  end
195
218
 
196
- def reset_onboarding!
197
- update!(
219
+ # Clear onboarding state so the user starts over.
220
+ #
221
+ # Milestones are cleared by default. achieve_milestone! is idempotent
222
+ # (it returns false for anything already in milestones_achieved), so
223
+ # leaving them behind means a user sent back through the flow can never
224
+ # re-earn them and finishes with no milestone notice at all. Pass
225
+ # clear_milestones: false to keep them - worth doing where a host app's
226
+ # milestones record real achievements rather than onboarding progress.
227
+ #
228
+ # Any in-flight replay is cancelled: a bare reset drops the user back to
229
+ # the default behaviour, where a step whose :complete_if already passes is
230
+ # advanced past automatically. Use restart_onboarding! to reset *and*
231
+ # replay.
232
+ #
233
+ # @param clear_milestones [Boolean] also wipe milestones, points and
234
+ # last_milestone_at (default: true)
235
+ # @return [Boolean] true
236
+ def reset_onboarding!(clear_milestones: true)
237
+ attributes = {
198
238
  onboarding_completed: false,
199
239
  onboarding_completed_at: nil,
200
240
  onboarding_skipped: false,
201
241
  onboarding_current_step: nil,
202
242
  feature_tooltips_shown: {}
203
- )
243
+ }
244
+ attributes.merge!(milestone_reset_attributes) if clear_milestones
245
+ attributes.merge!(replay_reset_attributes)
246
+
247
+ update!(attributes)
204
248
  end
205
249
 
206
250
  # Feature tooltips
@@ -432,11 +476,18 @@ module RailsOnboarding
432
476
  true
433
477
  end
434
478
 
435
- def restart_onboarding!(session_id: nil)
479
+ # Reset and immediately re-enter the flow at step one.
480
+ #
481
+ # Unlike a bare reset this turns replay mode on, so a user whose host-app
482
+ # data already satisfies every :complete_if walks the steps again instead
483
+ # of being auto-advanced straight back to "completed" on their next visit
484
+ # to /onboarding. See replaying_onboarding?.
485
+ def restart_onboarding!(session_id: nil, clear_milestones: true)
436
486
  previous_step = onboarding_current_step
437
487
  was_completed = onboarding_completed
438
488
 
439
- reset_onboarding!
489
+ reset_onboarding!(clear_milestones: clear_milestones)
490
+ start_onboarding_replay!
440
491
  start_onboarding!(session_id: session_id)
441
492
 
442
493
  AnalyticsEvent.track_custom_event(
@@ -447,6 +498,63 @@ module RailsOnboarding
447
498
  )
448
499
  end
449
500
 
501
+ # Replay mode
502
+ #
503
+ # A reset clears onboarding's own bookkeeping, but it cannot clear the
504
+ # host-app data a step's :complete_if reads. For an established user every
505
+ # predicate is still satisfied, so /onboarding advances through the whole
506
+ # flow in a single redirect and the reset looks like it did nothing.
507
+ #
508
+ # While a replay is active, :complete_if is no longer enough on its own -
509
+ # the user also has to have been shown the step again. That makes a
510
+ # restart walk the real pages a second time without touching the data
511
+ # those predicates read.
512
+
513
+ # Is this user currently re-walking the flow?
514
+ #
515
+ # False once they finish, so a completed user is never stuck in replay.
516
+ def replaying_onboarding?
517
+ return false unless self.class.onboarding_replay_supported?
518
+ return false if onboarding_completed?
519
+
520
+ onboarding_replay_started_at.present?
521
+ end
522
+
523
+ # @return [Boolean] false when the host app hasn't run the replay migration
524
+ def start_onboarding_replay!
525
+ return false unless self.class.onboarding_replay_supported?
526
+
527
+ update!(onboarding_replay_started_at: Time.current, onboarding_replay_steps: [])
528
+ true
529
+ end
530
+
531
+ def end_onboarding_replay!
532
+ return false unless self.class.onboarding_replay_supported?
533
+
534
+ update!(replay_reset_attributes)
535
+ true
536
+ end
537
+
538
+ # Has the user been shown this step since the current replay began?
539
+ #
540
+ # True whenever no replay is active, so callers can gate auto-advance on
541
+ # it unconditionally and get the ordinary behaviour outside a replay.
542
+ def onboarding_step_replayed?(step_name)
543
+ return true unless replaying_onboarding?
544
+
545
+ (onboarding_replay_steps || []).map(&:to_s).include?(step_name.to_s)
546
+ end
547
+
548
+ # Record that the user has now seen this step. No-op outside a replay.
549
+ def mark_onboarding_step_replayed!(step_name)
550
+ return false unless replaying_onboarding?
551
+ return false if onboarding_step_replayed?(step_name)
552
+
553
+ self.onboarding_replay_steps = (onboarding_replay_steps || []) + [ step_name.to_s ]
554
+ save!
555
+ true
556
+ end
557
+
450
558
  # API-friendly aliases and helper methods
451
559
  alias_method :complete_step, :complete_onboarding_step!
452
560
  alias_method :skip_step, :skip_onboarding_step!
@@ -552,6 +660,26 @@ module RailsOnboarding
552
660
  # triggers) must never fire for a state change that didn't actually get
553
661
  # persisted - update!/save! raise on failure, so if that happens, this
554
662
  # method never reaches the block.
663
+ # Milestone columns arrive in a separate migration, so a host app can be
664
+ # running without them. Only reset what actually exists.
665
+ def milestone_reset_attributes
666
+ attributes = {}
667
+ attributes[:milestones_achieved] = [] if onboarding_column?("milestones_achieved")
668
+ attributes[:milestone_points] = 0 if onboarding_column?("milestone_points")
669
+ attributes[:last_milestone_at] = nil if onboarding_column?("last_milestone_at")
670
+ attributes
671
+ end
672
+
673
+ def replay_reset_attributes
674
+ return {} unless self.class.onboarding_replay_supported?
675
+
676
+ { onboarding_replay_started_at: nil, onboarding_replay_steps: [] }
677
+ end
678
+
679
+ def onboarding_column?(name)
680
+ self.class.respond_to?(:column_names) && self.class.column_names.include?(name)
681
+ end
682
+
555
683
  def persist_and_track!(attributes = nil)
556
684
  if attributes
557
685
  update!(attributes)
@@ -60,6 +60,11 @@ module RailsOnboarding
60
60
  "db/migrate/add_robustness_fields_to_users.rb"
61
61
  end
62
62
 
63
+ def copy_onboarding_replay_migration
64
+ migration_template "add_onboarding_replay_to_users.rb",
65
+ "db/migrate/add_onboarding_replay_to_users.rb"
66
+ end
67
+
63
68
  def copy_initializer
64
69
  template "rails_onboarding.rb", "config/initializers/rails_onboarding.rb"
65
70
  end
@@ -87,6 +92,7 @@ module RailsOnboarding
87
92
  "add_milestone_tracking_to_users.rb",
88
93
  "add_onboarding_indexes.rb",
89
94
  "add_robustness_fields_to_users.rb.tt",
95
+ "add_onboarding_replay_to_users.rb",
90
96
  "rails_onboarding.rb",
91
97
  "onboarding.css",
92
98
  "README"
@@ -0,0 +1,22 @@
1
+ class AddOnboardingReplayToUsers < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def up
3
+ # Replay mode: when set, the user is walking the flow again after a
4
+ # restart, and a step's :complete_if is not allowed to advance them past a
5
+ # step they have not been shown yet. See Onboardable#replaying_onboarding?.
6
+ #
7
+ # No DB-level default on the text column: MySQL/MariaDB reject a literal
8
+ # DEFAULT on TEXT, and Onboardable already treats nil as an empty array.
9
+ unless column_exists?(:users, :onboarding_replay_started_at)
10
+ add_column :users, :onboarding_replay_started_at, :datetime
11
+ end
12
+
13
+ unless column_exists?(:users, :onboarding_replay_steps)
14
+ add_column :users, :onboarding_replay_steps, :text
15
+ end
16
+ end
17
+
18
+ def down
19
+ remove_column :users, :onboarding_replay_steps if column_exists?(:users, :onboarding_replay_steps)
20
+ remove_column :users, :onboarding_replay_started_at if column_exists?(:users, :onboarding_replay_started_at)
21
+ end
22
+ end
@@ -167,7 +167,9 @@ module RailsOnboarding
167
167
  "feature_tooltips_shown" => [ :jsonb, :text ],
168
168
  "milestones_achieved" => [ :text, :jsonb ],
169
169
  "milestone_points" => :integer,
170
- "last_milestone_at" => :datetime
170
+ "last_milestone_at" => :datetime,
171
+ "onboarding_replay_started_at" => :datetime,
172
+ "onboarding_replay_steps" => [ :text, :jsonb ]
171
173
  }
172
174
 
173
175
  # Check required columns
@@ -1,3 +1,3 @@
1
1
  module RailsOnboarding
2
- VERSION = "0.6.3"
2
+ VERSION = "0.7.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_onboarding
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Lewis
@@ -234,6 +234,7 @@ files:
234
234
  - lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb
235
235
  - lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb
236
236
  - lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb
237
+ - lib/generators/rails_onboarding/templates/add_onboarding_replay_to_users.rb
237
238
  - lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb
238
239
  - lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt
239
240
  - lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb
@@ -281,7 +282,7 @@ licenses:
281
282
  metadata:
282
283
  allowed_push_host: https://rubygems.org
283
284
  homepage_uri: https://github.com/bunnahabhain/rails_onboarding
284
- source_code_uri: https://github.com/bunnahabhain/rails_onboarding/tree/v0.6.3
285
+ source_code_uri: https://github.com/bunnahabhain/rails_onboarding/tree/v0.7.0
285
286
  changelog_uri: https://github.com/bunnahabhain/rails_onboarding/blob/master/docs/CHANGELOG.md
286
287
  rdoc_options: []
287
288
  require_paths: