How do I detect the first time the user logs in and the first time a specific page is loaded?
Asked Answered
A

8

11

I would like to Trigger some JS only the first time a user logs in, and only the first time a specific page is loaded.

I believe I can deal with the first time they log in, by simply checking user.sign_in_count < 2, but I don't know how to specify just on the first page load only.

i.e. I don't want the JS to be triggered after the user logs in for the first time and refreshes the page without logging out.

I am using Turbolinks and $(document).on('turbolinks:load', function() { to trigger it.

Edit 1

So what I am trying to do is execute Bootstrap Tour on a number of pages. But I only want that tour to be automatically executed, on the first page load. The tour itself will lead the user to other specific pages within my app, but each of those pages will have page-specific tour JS on each page.

Right now, in my HTML I have something like this:

<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});

      // Initialize the tour
      tour.init();

      // Start the tour
      tour.start();
  });
</script>

So all I really want to do is the following:

  • Not bombard the user with executing a new tour, on their first login, whenever they reload the page.
  • Allow them to be able to manually execute the tour at a later date if they want, by simple pressing a link.
  • I don't want to store anything in my DB if I don't have to -- so preferably this should be a cookie-based approach or localStorage
  • Assume that I will use Rails to track the number of sign-ins they have done. So once they sign in more than once, I can not trigger this JS.

The real problem is just within that first sign in, if they refresh the main page 10 times, this tour gets executed 10 times. That's what I am trying to stop.

I hope that provides some more clarity.

Adolescence answered 4/12, 2016 at 23:48 Comment(7)
Do you want an event to fire on a specific page just once in that user's "lifetime", or once for each session (meaning if the user comes back to that page a week later, will should the event fire)?Scalariform
@Scalariform Once in the user's lifetime. It should not automatically fire a week later, but I believe I can manage that with my user.sign_in_count > 1 check. So presumably, once they login more than once they won't see it. The issue I am struggling with right now is just preventing the tour from firing for every page load while they were logged in the first time.Adolescence
Its not the answer you want but from a User Experience POV you should store this in the database for the user's entry. This will allow it to exist cross browser sessions, i.e. if user opens Chrome, turns off tour, then later opens Firefox say on different computer, you'll still know not to present them with the tour. You're overthinking it by constraining yourself from hitting the db IMO.Faultfinder
@engineerDave, if he's using "login count" which would come from the DB combined with a localStorage solution, is there ever a scenario where going to firefox on a different computer would actually present them with the tour? (the user would still have to login again, which would increase the login count to > 1)Marismarisa
@Marismarisa as localStorage is specific to a browser https://mcmap.net/q/719234/-html5-local-storage-across-different-browsers the short answer from a UX perspective is Yes. (Unless you want to assume a login count of > 0 means present no tour.) The end user would end up being presented with the tour in each new browser that they use on the same computer or a different machine.Faultfinder
@engineerDave, from what OP has asked, login count > 0 means no tour :)Marismarisa
@marcamillion, care to mark an answer before the bounty expires?Marismarisa
M
15

Preface

It's my understanding that you have:

  1. multiple pages that contain a single tour (each page's tour is different)
  2. a way to detect first signin to an account (ruby login count)
  3. ability to add a script value based upon first signin

Solution Overview

The solution below uses localStorage to store a key value pair of each tour's identifier and if it has been seen or not. localStorage persists between page refreshes and sessions, as the name suggests, localStorage is unique to each domain, device, and browser (ie. chrome's localStorage cannot access firefox's localStorage even for the same domain, nor can chrome's localStorage on your laptop access chrome's localStorage on your mobile even for the same domain). I raise this to illustrate the reliance upon Preface 3 to toggle a JS flag for if the user has logged in previously.

For the tour to start, the code checks localStorage for if its corresponding key value pair is not set to true (representing having been "seen"). If it does exist and is set to true, the tour does not start, otherwise it runs. When each tour begins, using its onStart method, we update/add the tour's identifier to localStorage and set its value to true.

Manual execution of the tour can be performed by either manually calling the tour's start method if you would like only the current page's tour to execute, otherwise, you can clear out all of the localStorage related to the tour and send the user back to the first page/if you're on the first page, again just call the start method.

JSFiddle (HTML based off other question's you've asked regarding touring)

HTML (this could be any element with the id="tourAgain" attribute for the following code to work.

<button class="btn btn-sm btn-default" id="tourAgain">Take Tour Again</button>

JS

var isFirstLogin = true; // this value is populated by ruby based upon first login
var userID = 12345; // this value is populated by ruby based upon current_user.id, change this value to reset localStorage if isFirstLogin is true
// jquery on ready function
$(function() {
    var $els = {};  // storage for our jQuery elements
    var tour; // variable that will become our tour
    var tourLocalStorage = JSON.parse(localStorage.getItem('myTour')) || {};

    function activate(){
        populateEls();
        setupTour();
        $els.tourAgain.on('click', tourAgain);
        // only check check if we should start the tour if this is the first time we've logged in
        if(isFirstLogin){
            // if we have a stored userID and its different from the one passed to us from ruby
            if(typeof tourLocalStorage.userID !== "undefined" && tourLocalStorage.userID !== userID){
                // reset the localStorage
                localStorage.removeItem('myTour');
                tourLocalStorage = {};
            }else if(typeof tourLocalStorage.userID === "undefined"){ // if we dont have a userID set, set it and save it to localStorage
                tourLocalStorage.userID = userID;
                localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
            }
            checkShouldStartTour();
        }
    }

    // helper function that creates a cache of our jQuery elements for faster lookup and less DOM traversal
    function populateEls(){
        $els.body = $('body');
        $els.document = $(document);
        $els.tourAgain = $('#tourAgain');
    }

    // creates and initialises a new tour
    function setupTour(){
        tour = new Tour({
            name: 'homepage', // unique identifier for each tour (used as key in localStorage)
            storage: false,
            backdrop: true,
            onStart: function() {
                tourHasBeenSeen(this.name);
                $els.body.addClass('is-touring');
            },
            onEnd: function() {
                console.log('ending tour');
                $els.body.removeClass('is-touring');
            },
            steps: [{
                element: "div.navbar-header img.navbar-brand",
                title: "Go Home",
                content: "Go home to the main page."
            }, {
                element: "div.navbar-header input#top-search",
                title: "Search",
                content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
            }, {
                element: "span.num-players",
                title: "Number of Players",
                content: "This is the number of players that are in our database for this Tournament"
            }, {
                element: '#page-wrapper div.contact-box.profile-24',
                title: "Player Info",
                content: "Here we have a quick snapshot of the player stats"
            }]
        });
        // Initialize the tour
        tour.init();
    }

    // function that checks if the current tour has already been taken, and starts it if not
    function checkShouldStartTour(){
        var tourName = tour._options.name;
        if(typeof tourLocalStorage[tourName] !== "undefined" && tourLocalStorage[tourName] === true){
            // if we have detected that the tour has already been taken, short circuit
            console.log('tour detected as having started previously');
            return;
        }else{
            console.log('tour starting');
            tour.start();
        }
    }

    // updates localStorage with the current tour's name to have a true value
    function tourHasBeenSeen(key){
        tourLocalStorage[key] = true;
        localStorage.setItem('myTour', JSON.stringify(tourLocalStorage));
    }

    function tourAgain(){
        // if you want to tour multiple pages again, clear our localStorage 
        localStorage.removeItem('myTour');
        // and if this is the first part of the tour, just continue below otherwise, send the user to the first page instead of using the function below
        // if you just want to tour this page again just do the following line
        tour.start();
    }

    activate();
});

PS. the reason we dont use onEnd to trigger the tourHasBeenSeen function is that there is currently a bug with bootstrap tour where if the last step's element doesnt exist, the tour ends without triggering the onEnd callback, BUG.

Marismarisa answered 11/12, 2016 at 6:22 Comment(8)
Oh wow....thanks for this extensive answer @haxxxton. What is "Preface 3"? One use-case I just realized that does need to be covered is if we have two users login for the first time on the same computer. Imagine there is one computer in the office that they share or perhaps they used someone else's computer (that has already taken the tour) to login for the first time. How do we ensure that the tour is executed if current_user.sign_in_count < 2 regardless of whether localStorage is set/created?Adolescence
@marcamillion, preface 3 relates to the third point made in the preface "ability to add a script value based upon first signin". Do you have access to a "unique" identifier for the user? ie. something like userId? you would just include that value as part of the localStorage, and if isFirstLogin is true, but the userId does not match that of the localStorage clear out localStorage using the localStorage.removeItem('myTour'); line :)Marismarisa
Ahh ok...I get you. Yeh that makes sense.Adolescence
@marcamillion, if you let me know if you have access to a unique identifier, i can update my code to include this :)Marismarisa
You can assume it is current_user.id. That's my Rails variable. I can set that in JS though.Adolescence
@marcamillion, updated to include checks for userID, and fixed a bug around the isFirstLogin checking logicMarismarisa
I haven't gotten a chance to test this yet, been a crazy last few days. But I awarded you the bounty just based on the quality of the answer. Can you check back periodically in case I have any questions when I do test it please? I am going to try and get to it this weekend. Thanks!Adolescence
@marcamillion, of course brother, just @ tag me and it should come throughMarismarisa
C
6

You could try using Javascript's sessionStorage, which is deleted when the user closes the tab, but survives through refreshes. Just use sessionStorage.setItem(key, value and sessionStorage.getItem(key). Remember that sessionStorage can only store strings!


Using your code:
<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});
      if(sessionStorage.getItem("loggedIn") !== "yes"){//Remember that sessionStorage can only store strings!
        //Initialize the tour
        tour.init();
        // Start the tour
        tour.start();
      }
      else{
        //Set item "loggedIn" in sessionStorage to "yes"
        sessionStorage.putItem("loggedIn", "yes");
      }
      var goBackToTour = function(e){
        //You can also make a "fake" link, so that it looks like a link, but is not, and you don't have to put the following line:
        e.preventDefault();
        tour.init();
        tour.start();
      };
      document.getElementById("goBackToTourLink").addEventListener("click", goBackToTour);
  });
  //On the logout
  var logout = function(){
    sessionStorage.setItem("loggedIn", "no");
  };
</script>
Cataphoresis answered 10/12, 2016 at 17:17 Comment(2)
Can you give a more complete example using the code I put in the question for clarity's sake please?Adolescence
It's there now.Cataphoresis
O
4

You can store if user has seen the tour or not in the cookie. You can maintain a "TrackingCookie" which has all the user tracking information (eg. tour_shown, promotion_shown etc, which is accessed by your javascript code. Following TrackingCookie code is to maintain all such tracking information in one cookie. I am calling it tracking_cookie.

Cookies can be accessed server-side using

cookies[:tracking_cookie]

tracking_cookie.js

var TrackingCookie = (function() {
  function TrackingCookie() {
    this.name = 'tracking_cookie';
    this.expires = new Date(new Date().setYear(new Date().getFullYear() + 1));
  }

  TrackingCookie.prototype.set = function(name, value) {
    var data={};
    if(!this.readFromStore()) {
      data = this.readFromStore();
    }
    data[name] = value;
    return this.writeToStore(data);
  };

  TrackingCookie.prototype.set_if_unset = function(name, value) {
    if (!this.get(name)) {
      return this.set(name, value);
    }
  };

  TrackingCookie.prototype.get = function(name) {
    return this.readFromStore()[name];
  };

  TrackingCookie.prototype.writeToStore = function(data) {
    return $.cookie(this.name, JSON.stringify(data), {
      path: '/',
      expires: this.expires
    });
  };

  TrackingCookie.prototype.readFromStore = function() {
    return $.parseJSON($.cookie(this.name));
  };

  return TrackingCookie;

})();

In your HTML

<script type="text/javascript">
  $(document).on('turbolinks:load', function() {
    //Instantiate the cookie
    var tracking_cookie = new TrackingCookie();
    //Cookie value not set means, it is a new user.
    if(!tracking_cookie.get("tour_shown")){
      //Set the value to be true.
      tracking_cookie.set("tour_shown",true)
      var tour = new Tour({
        storage: false,
        backdrop: true,
        onStart: function(){
        $('body').addClass('is-touring');
        },
        onEnd: function(){
        $('body').removeClass('is-touring');
        },
        steps: [
        {
          element: "#navbar-logo",
          title: "Go Home",
          content: "All throughout the app, you can click our logo to get back to the main page."
        },
        {
          element: "input#top-search",
          title: "Search",
          content: "Here you can search for players by their name, school, positions & bib color (that they wore in our tournament)"
        }
      ]});

      // Initialize the tour
      tour.init();

      // Start the tour
      tour.start();
    };

  });
</script>

The cookie class is verbose. You can just use $.cookie to achieve simple one toggle behavior. The above code works for all first time users, logged-in as well as logged-out. If you just want it for logged-in user, set the flag on user log-in on server-side.

Osteoarthritis answered 10/12, 2016 at 9:38 Comment(2)
This is an interesting approach, it just feels very heavy-handed to me.Adolescence
I prefer the local storage solution that others have suggested. Much more lightweight and simpler to implement. Thanks for the effort though. I have upvoted you for the effort.Adolescence
C
4

To use local storage:

  if (typeof(Storage) !== "undefined") {
    var takenTour = localStorage.getItem("takenTour");
    if (!takenTour) {
      localStorage.setItem("takenTour", true);
      // Take the tour
    }
  }

We use this solution because our users don't log in, and it is a bit lighter than using cookies. As mentioned above it doesn't work when users switch machines or clear the cache, but you have that covered off by your login count.

Coati answered 10/12, 2016 at 15:4 Comment(3)
I love this approach. I tested it and it seems to be working. So the only missing part of this is, how do I manually execute the tour from a link?Adolescence
@Adolescence - I think you already have all the pieces in place, to start the tour from a link. You can move the code you have for initializing and starting the tour into a separate function and call it when the link is clicked (as well as for first login). $(".tour-link").click(function(e) { if (tour) { tour.restart(); } else { initTour(); } }Coati
Of all answers, I still think using localStorage as described in this answer is the most succinct, straightforward, and forward-thinking.Humidity
S
2

Based on your comment, I think you're going to want to track this in your data (which is effectively what you're doing with the user.sign_in_count > 1 check). My recommendation would be to use a lightweight key-value data store like Redis.

In this model, each time a user visits a page that has this feature, you check for a "visited" value associated with that user in Redis. If it doesn't exist, you trigger the JS event and add "visited": true to Redis for that user, which will prevent the JS from triggering in the future.

Scalariform answered 8/12, 2016 at 6:39 Comment(4)
That's kinda what I had in mind, but that feels a bit too heavy for my needs. All I really want is some lightweight JS that can detect if the user has ever loaded this page, so it pushes the detection to be client-side, rather than server-side.Adolescence
I hear you. The problem is, you can't guarantee it will always be accurate with a front-end only solution. For all you know the user could use a different computer entirely the second time they access your app. Redis (or similar types of data stores) is actually pretty lightweight (it's often used for server-side caching). The key-value structure of it also makes it quite flexible - easy to manipulate later on if things change. If you really want/need something that's only front-end, then utilizing localStorage/cookies would be your best bet.Scalariform
Well yeh, that's the point. If the user logs in another time, I don't want them to see the tour. They only see it on their first login. if they want to manually trigger the tour after they have logged in twice, then they can do so. But I don't want the system automatically launching the tour if they have logged in more than once. Using localStorage in the browser is the most lightweight version for this.Adolescence
Yeah, we're on the same page. I just want to stress that the localStorage solution is not a guaranteed one. In in number of scenarios - even one as simple as a user accessing you app in Firefox the first time and Chrome the second - relying only on localStorage will not work.Scalariform
E
2

Local storage is not a cross browser solution. Try this cross browser SQL implementation which uses different methods (including localstorage) to store 'databases' on the users hard drive indefinitely.

var visited;
jSQL.load(function(){
  // create a table
  jSQL.query("create table if not exists visits (time date)").execute();

  // check if the user visited
  visited = jSQL.query("select * from visits").execute().fetchAll("ASSOC").length;

  // update the table so we know they visited already next time
  jSQL.query("insert into visits values (?)").execute([new Date()]);

  jSQL.persist();
});
Endocrine answered 13/12, 2016 at 19:11 Comment(1)
could you please add any docs about localstorage and which browsers it is/isnt compatible with?Marismarisa
M
1

This should work if what you want to do is gate the page for its life. If you need to prevent re-execution for longer periods, consider localStorage.

var triggered;
$(document).on('turbolinks:load', function() {
if (triggered === undefined) {
            triggered = "yes";
...code...
}}
Montserrat answered 5/12, 2016 at 2:50 Comment(4)
I like the idea, but for whatever reason, it doesn't work. When I reload the page, it still triggers the code within the if (triggered == undefined)... conditional.Adolescence
I just did some quick console.logs right after the variable initialization and then after the value was updated and everytime I reload the page, I see both values for triggered. Both undefined and yes, indicating to me that triggered doesn't keep the yes value on page reload. The only time it doesn't trigger is if I go back, which leads me to believe that turbolinks:load is not called again. But once I hard refresh, it executes the JS.Adolescence
@Adolescence I am using that gate successfully for jQuery Mobile, but not Turbolinks. Both use AJAX to update the page body so they are similar in objective if not implementation. My particular implementation is actually an SPA, which could make a difference. FWIW, there should be three equal signs but that probably wouldn't make a difference. I just don't see why it wouldn't work. If it doesn't, you could use localStorage, but that could get messy with unexpected artifacts if it's not cleaned up with logout or a similar implementation.Montserrat
Yeh I think it is TurboLinks related, I suspect. But I am not quite sure what it is, just yet. I am using the triple =.Adolescence
D
1

You're going to have to communicate with the backend somehow to get sign-in count. Either in a injected variable, or as json route you hit with ajax, do logic like:

if !session[:seen_tour] && current_user.sign_in_count == 1
  @show_tour = true
  session[:seen_tour] = true
else
  @show_tour = false
end

respond_to do |format|
  format.html {}
  format.json { render json: {show_tour: @show_tour } }
end

Values in session will persist however you've configured your session store, by default that is stored in cookies.

Diverticulitis answered 8/12, 2016 at 19:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.