Technical Support manager at @heroku. Part of the @madrockclimbing climbing team.

User Time Zone On Signup

Knowing a user’s time zone can be incredibly important to your application. At SproutMark we utilize this information in numerous places to enhance the user’s experience.

But it’s difficult to know when to ask for it. If you ask during sign up, you expand the signup form an can reduce your signup’s because of the increased barrier to entry. If you wait until after sign up, you create an experience for the user that’s less than ideal.

Instead, let’s determine the user’s time zone based on their IP. We can set a sane default for the user and then allow them to change it. Let’s take a look at how we can do that with rails.

(I’ll be taking you through a pure Ruby implementation first … then the final JS-only version at the end)

The Guts

We’re going to use a combo of two gem’s to determine a user’s time zone.

# Gemfile
gem 'geocoder'
gem 'timezone'

We’ll use the Geocoder gem to turn our user’s IP into coordinates. Now Geocoder is awesome, but it doesn’t give us any time zone data. So we’ll then need to pass these lat/long coordinates to something else to get the time zone. Enter the Time Zone gem. (Clever name, eh?)

module SignupHelper

  def time_zone_from_ip
    begin
      ip_address = Rails.env.production? ? request.ip : "108.245.160.41"  # In development, our IP will be 127.0.0.1 ... not useful
      geocode = Geocoder.search(ip_address).first
      Timezone::Zone.new(latlon: [geocode.latitude, geocode.longitude]).active_support_time_zone
    rescue Exception => e
      Rails.logger.error "ERROR WITH GEOCODING: #{e.message}"
      "UTC"
    end
  end

end

…and the view…

.form-group
  = f.label :time_zone, "Time Zone"
  = f.collection_select :time_zone, ActiveSupport::TimeZone.all, :name, :to_s, { selected: f.object.time_zone || time_zone_from_ip }, class: "chosen-select"

In short, we’re defaulting to the object’s value (nil by default, or set in case this form was submitted with other errors). If the value is nil, we’re calling our helper to determine the user’s time zone.

Hide The Dropdown

This is pretty slick, but I’m not too happy with how this form appears to the user. Even though we’re setting their default and the user can easily skip this select box, it’s mentally taxing to the user to see a form field and process whether or not they need to change the value. How can we show the default, but still allow the user to change it? Let’s make it a link that toggles the select box.

.form-group
  = f.label :time_zone, "Time Zone"

  p= link_to f.object.time_zone || "UTC", "javascript:;", data: { "time-zone" => "toggle" }

  .hide
    = f.collection_select :time_zone, ActiveSupport::TimeZone.all, :name, :to_s, { selected: f.object.time_zone }, class: "chosen-select"

Now for the JS. When a user clicks the link, we’ll hide it and then display the select box.

$("a[data-time-zone='toggle']").click ->
    $(@).hide()
    $(@).parent().next(".hide").removeClass("hide")
    $(".chosen-container").css("width", "340px")

Non-Blocking

One more problem we’ve got: a blocking HTTP request inside our signup form. In order to have the best chance at converting a visitor, we need this page to load FAST. And waiting for this IP check is hurting us.

Instead of making the form wait on this request, we can background it by loading the form with sane defaults, then making another AJAX request back to the server for the time zone info.

# SignupController.rb

  def determine_time_zone
    render json: { time_zone: time_zone_from_ip }
  end

private
  def time_zone_from_ip
    begin
      ip_address = Rails.env.production? ? request.ip : "108.245.160.41"
      geocode = Geocoder.search(ip_address).first
      Timezone::Zone.new(latlon: [geocode.latitude, geocode.longitude]).active_support_time_zone if geocode
    rescue Exception => e
      Rails.logger.error "ERROR WITH GEOCODING: #{e.message}"
      "UTC"
    end
  end

And now the JS. If our new div ID is on the page, let’s use jQuery’s getJSON to fetch this user’s time zone as JSON. We’ll then update the default link text, show the link, update our select box, and reset Chosen.

if $("#signup_time_zone_group").length > 0
  $.getJSON "/determine_time_zone", (json) ->
    $("#signup_time_zone_group").find("p a").html(json.time_zone)
    $("#signup_time_zone_group").removeClass("hide")
    $("#signup_time_zone").val(json.time_zone)
    $("#signup_time_zone").trigger("chosen:updated")
    return

There’s Got To Be A Better Way

After having this implementation in production for a few days, I wrote up this post touting how simple this Ruby implementation was, and how terribly awful trying to do this in pure JS was. Then, magically, a simpler solution showed itself.

While researching a JS version, I ran across jstz – a little library that grabs a user’s time zone from their browser. The problem with this was the fact that the library returns a IANA zone info key (aka the Olson time zone database). This is not the same key that Rails uses. I scrapped this route based on this information.

However, today I managed to find a neat little piece of JS that converts from IANA to the Rails time zones: rails-timezone-js. I dropped in this converter along with jstz, and we have a much simpler solution:

  if $("#signup_time_zone_group").length > 0
    rails_timezone = window.RailsTimeZone.to(jstz.determine().name())
    $("#signup_time_zone_group").find("p a").html(rails_timezone)
    $("#signup_time_zone_group").removeClass("hide")
    $("#signup_time_zone").val(rails_timezone)
    $("#signup_time_zone").trigger("chosen:updated")

You’re notice there’s now no AJAX call back to the server. We use jstz to grab the time zone name, then pass it off to the converter helper method: rails_timezone = window.RailsTimeZone.to(jstz.determine().name()). BOOM!

Conclusion

After getting stuck on a pure JS version, I went down the path of using Ruby and a little AJAX… but then managed to find a solution to the JS version. Regardless of how the time zone was determined, the more interesting part of this process for me was the UX.

My first version didn’t solve the main problem of reducing cognitive load. So I hid the dropdown. This solved the user-facing issue, but the process of determining the user’s lat/lng was creating slow load times. We fixed this by “backgrounding” this part of the process, but that left us with a lot of Ruby for a problem that I was sure had to have been solved before. Sure enough, rails-timezone-js to the rescue!

To see this in action, view SproutMark’s signup form. If you have any tips, let me know in the comments!

Much thanks to Jeff Berg for helping me with this. He and his team at Planning Center use a similar implementation.

© 2017 Jon McCartie