Main site

Built-in Modules > getVisitorLocation Function

getVisitorLocation Function

Overview

The getVisitorLocation function utilizes IP geolocation to determine the location of a visitor, providing comprehensive geographical details.

Function Definition

function getVisitorLocation() {
  // Function to get visitor location
}

Usage Example

import { getVisitorLocation } from "./index.js"
const location = getVisitorLocation();

In this example, getVisitorLocation is called without any arguments to retrieve the location of the visitor based on their IP address.

Sample Output

{
  "country": "US",
  "city": "Chicago",
  "postalCode": "60602",
  "region": "Illinois",
  "regionCode": "IL",
  "timezone": "America/Chicago",
  "longitude": "-87.63180",
  "latitude": "41.88740",
  "continent": "NA"
}

Notes

  • The getVisitorLocation function is imported from the module "index.js".
  • This function is particularly useful for tailoring user experiences based on geographical location, such as customizing content, setting the default language, or adjusting currency and shipping options.
  • It provides detailed location information, including country, city, postal code, region, timezone, longitude, latitude, and continent, enabling precise localization.
  • For accurate results, it should be used for requests received via the direct JsRates API endpoint (not the Shopify proxy). See the important note below.

Important: Direct API Endpoint Required

getVisitorLocation detects the visitor's location from the IP address of the incoming request. This means the HTTP request must originate directly from the visitor's browser.

Do not use the Shopify proxy (/apps/jsrates/rates) for this purpose. Shopify proxies requests through its own servers, so getVisitorLocation will detect Shopify's server IP — not the visitor's real location.

Instead, call the direct JsRates API endpoint from your storefront JavaScript. The request goes straight from the visitor's browser to JsRates, preserving the real IP address.

POST https://api.jsrates.com/shopify/{store-name}-shipping
Content-Type: application/json
x-jsrates-shop-domain: your-store.myshopify.com
x-jsrates-api-key: your-api-key-from-settings

Your exact endpoint URL is shown in JsRates Settings → API section.

Full Usage Example: Auto-Detecting Visitor ZIP on a Product or Cart Page

A common use case is showing real-time shipping rates on a product or cart page, pre-populated with the visitor's detected location. The visitor can also override the ZIP with their own.

This requires two parts: the JsRates server-side code and the Shopify theme (Liquid) frontend code.

How It Works

  1. The storefront sends a request to the direct JsRates API with an empty destination postal code.
  2. JsRates calls getVisitorLocation() server-side, detects the visitor's ZIP from their IP, and uses it for rate calculation.
  3. JsRates returns the rates along with the detected location in _detectedLocation.
  4. The storefront displays the detected ZIP and the shipping rates. The visitor can override the ZIP to re-fetch rates for a different location.

Part 1 — JsRates Code (calculateShippingRates.js)

Paste this into the JsRates editor. It detects the visitor's location, falls back to the destination postal code if already provided, and returns the detected location alongside the rates.

import { getVisitorLocation } from "./index.js";

export async function calculateShippingRates(DATA, env) {
  try {
    // Detect the visitor's real location from their browser IP.
    // This works because the storefront calls the direct JsRates API endpoint,
    // not the Shopify proxy — so the real visitor IP is preserved.
    const visitor = getVisitorLocation();

    // Use the visitor's detected ZIP if the destination was not provided.
    // When the user manually enters a ZIP on the frontend, it will be
    // passed as DATA.destination.postal_code and used instead.
    const zip     = DATA.destination.postal_code || visitor?.postalCode || "";
    const country = DATA.destination.country     || visitor?.country    || "US";

    // --- Your rate logic here ---
    // Use `zip` and `country` to calculate rates.
    // Replace the example below with your own logic, live carrier API calls, etc.
    const rates = [
      {
        service_name: "Standard Shipping",
        service_code: "STD",
        total_price:  799,
        description:  "5–7 business days",
        currency:     DATA.currency,
      },
      {
        service_name: "Express Shipping",
        service_code: "EXP",
        total_price:  1499,
        description:  "2–3 business days",
        currency:     DATA.currency,
      },
    ];

    // Return the rates and pass the detected location back to the frontend.
    // The frontend reads `_detectedLocation` to display the visitor's city and ZIP.
    return {
      rates,
      _detectedLocation: {
        postalCode: visitor?.postalCode || "",
        city:       visitor?.city       || "",
        regionCode: visitor?.regionCode || "",
        country:    visitor?.country    || "",
      },
    };

  } catch (error) {
    console.error("calculateShippingRates error:", error.message);
    return {
      rates: [{
        service_name: "Standard Shipping",
        service_code: "FALLBACK",
        total_price:  999,
        description:  "Estimated shipping rate",
        currency:     DATA.currency,
      }],
    };
  }
}

Part 2 — Shopify Theme Code

The frontend consists of two files: a data snippet that uses Liquid to populate the rate request payload from Shopify's product/cart context, and a widget snippet that reads that data and makes the API call.

File A: snippets/jsrates-data.liquid

This snippet sets window.jsRatesData using Liquid — giving the widget access to fully-populated origin, destination, and item data rendered server-side by Shopify. Add it to the bottom of theme.liquid, just before the closing </body> tag.

{% comment %}
  Renders the JsRates rate request data for use by the zip widget.

  Usage:
  {%  render 'jsrates-data' %}

  Insert this at the bottom (just before closing body tag) of "theme.liquid".
{% endcomment %}

{% if template == 'product' %}
  <script>
    window.jsRatesData = {
      "rate": {
        "origin": {
          "country": `{{ shop.address.country.iso_code }}`,
          "city": `{{ shop.address.city }}`,
          "postal_code": `{{ shop.address.zip }}`,
          "province": `{{ shop.address.province }}`,
          "name": `{{ shop.name }}`,
          "address1": `{{ shop.address.address1 }}`,
          "address2": `{{ shop.address.address2 }}`,
          "company_name": `{{ shop.name }}`
        },
        "destination": {
          "country": `{{ localization.country.iso_code }}`,
          "city": null,
          "postal_code": null,
          "province": null,
          "name": null,
          "address1": null,
          "address2": null
        },
        "items": [
          {
            "name": `{{ product.title }} - {{ product.selected_or_first_available_variant.title }}`,
            "sku": `{{ product.selected_or_first_available_variant.sku }}`,
            "grams": {{ product.selected_or_first_available_variant.weight }},
            "quantity": 1,
            "price": {{ product.selected_or_first_available_variant.price }},
            "vendor": `{{ product.vendor }}`,
            "requires_shipping": {{ product.selected_or_first_available_variant.requires_shipping }},
            "taxable": {{ product.selected_or_first_available_variant.taxable }},
            "fulfillment_service": `{{ product.selected_or_first_available_variant.fulfillment_service }}`,
            "product_id": {{ product.id }},
            "variant_id": {{ product.selected_or_first_available_variant.id }}
          }
        ],
        "currency": `{{ localization.country.currency.iso_code }}`,
        "locale": `{{ localization.language.iso_code }}`
      }
    };
    window.jsRatesCountry = `{{ localization.country.name }}`;
  </script>
{% endif %}

File B: snippets/jsrates-zip-widget.liquid

This snippet renders the ZIP widget UI and makes the API call using window.jsRatesData populated above. Place it wherever you want the widget to appear on the product page, then include it with:

{% render 'jsrates-zip-widget' %}

Update the three configuration variables at the top before use.

{% comment %}
  JsRates ZIP Code Widget
  Reads window.jsRatesData set by jsrates-data.liquid.
  Calls the direct JsRates API endpoint so that getVisitorLocation()
  detects the real visitor IP (not a Shopify proxy server IP).

  Usage:
  {% render 'jsrates-zip-widget' %}
{% endcomment %}

{% assign jsrates_endpoint    = "https://api.jsrates.com/shopify/your-store-shipping" %}
{% assign jsrates_shop_domain = "your-store.myshopify.com" %}
{% assign jsrates_api_key     = "YOUR_API_KEY_FROM_JSRATES_SETTINGS" %}

<div id="jsrates-widget">
  <p id="jsr-status">Detecting your location…</p>
  <div id="jsr-display" style="display:none">
    <p id="jsr-location-label"></p>
    <button id="jsr-zip-btn"></button>
  </div>
  <div id="jsr-edit" style="display:none">
    <input id="jsr-input" type="text" inputmode="numeric" maxlength="10" placeholder="Enter ZIP" />
    <p id="jsr-input-error" style="color:red"></p>
    <button id="jsr-confirm-btn">Confirm</button>
    <button id="jsr-cancel-btn">Cancel</button>
  </div>
  <div id="jsr-rates" style="display:none">
    <p>Shipping to <strong id="jsr-rates-zip"></strong></p>
    <div id="jsr-rates-list"></div>
  </div>
  <p id="jsr-error" style="color:red;display:none"></p>
</div>

<script>
(function () {
  // ── Configuration (set by Liquid above) ───────────────────────
  var ENDPOINT = {{ jsrates_endpoint    | json }};
  var SHOP     = {{ jsrates_shop_domain | json }};
  var API_KEY  = {{ jsrates_api_key     | json }};
  // ─────────────────────────────────────────────────────────────

  var currentZip       = "";
  var detectedLocation = null;
  var isAutoDetected   = true;

  function buildPayload(zip) {
    // Start from window.jsRatesData populated by jsrates-data.liquid,
    // which contains fully-rendered origin and item data from Shopify Liquid.
    var payload = JSON.parse(JSON.stringify(window.jsRatesData));

    // Override destination postal code with the user-supplied or detected ZIP.
    // On first load this is empty — JsRates will use getVisitorLocation() instead.
    payload.rate.destination.postal_code = zip || null;

    return payload;
  }

  async function fetchRates(zip) {
    document.getElementById("jsr-status").textContent    = "Loading rates…";
    document.getElementById("jsr-error").style.display   = "none";
    document.getElementById("jsr-rates").style.display   = "none";
    document.getElementById("jsr-display").style.display = "none";

    try {
      // Call the direct JsRates API endpoint — NOT the Shopify proxy (/apps/jsrates/rates).
      // The Shopify proxy routes through Shopify's servers, causing getVisitorLocation()
      // to detect Shopify's server IP instead of the real visitor IP.
      // The direct endpoint receives the request straight from the browser,
      // so the visitor's real IP is preserved for getVisitorLocation().
      var resp = await fetch(ENDPOINT, {
        method: "POST",
        headers: {
          "Content-Type":          "application/json",
          "x-jsrates-shop-domain": SHOP,
          "x-jsrates-api-key":     API_KEY
        },
        body: JSON.stringify(buildPayload(zip))
      });

      if (!resp.ok) throw new Error("HTTP " + resp.status);
      var data = await resp.json();

      // On first load (no zip provided), read the detected location returned
      // by getVisitorLocation() running server-side in JsRates.
      if (!zip && data._detectedLocation && data._detectedLocation.postalCode) {
        detectedLocation = data._detectedLocation;
        currentZip       = detectedLocation.postalCode;
        isAutoDetected   = true;
      }

      renderDisplay();
      renderRates(data.rates || []);

    } catch (err) {
      document.getElementById("jsr-status").textContent  = "";
      document.getElementById("jsr-error").textContent   = "Could not load shipping rates.";
      document.getElementById("jsr-error").style.display = "block";
    }
  }

  function renderDisplay() {
    document.getElementById("jsr-status").textContent        = "";
    document.getElementById("jsr-display").style.display     = "block";
    document.getElementById("jsr-zip-btn").textContent       = currentZip + " (change)";
    var meta = isAutoDetected && detectedLocation
      ? "📡 Auto-detected: " + (detectedLocation.city || "") + (detectedLocation.regionCode ? ", " + detectedLocation.regionCode : "")
      : "✏️ Manually set";
    document.getElementById("jsr-location-label").textContent = meta;
  }

  function renderRates(rates) {
    if (!rates.length) return;
    document.getElementById("jsr-rates-zip").textContent = currentZip;
    document.getElementById("jsr-rates-list").innerHTML  = rates.map(function(r) {
      var price = r.total_price === 0 ? "FREE" : "$" + (r.total_price / 100).toFixed(2);
      return "<div><strong>" + r.service_name + "</strong> — " + price
        + (r.description ? " <small>(" + r.description + ")</small>" : "") + "</div>";
    }).join("");
    document.getElementById("jsr-rates").style.display = "block";
  }

  // Edit ZIP
  document.getElementById("jsr-zip-btn").addEventListener("click", function() {
    document.getElementById("jsr-display").style.display = "none";
    document.getElementById("jsr-edit").style.display    = "block";
    document.getElementById("jsr-input").value           = currentZip;
    document.getElementById("jsr-input").focus();
  });

  document.getElementById("jsr-confirm-btn").addEventListener("click", function() {
    var val = document.getElementById("jsr-input").value.trim();
    if (!/^\d{5}(-\d{4})?$/.test(val)) {
      document.getElementById("jsr-input-error").textContent = "Please enter a valid 5-digit ZIP.";
      return;
    }
    document.getElementById("jsr-input-error").textContent = "";
    document.getElementById("jsr-edit").style.display      = "none";
    currentZip     = val;
    isAutoDetected = false;
    fetchRates(currentZip);
  });

  document.getElementById("jsr-cancel-btn").addEventListener("click", function() {
    document.getElementById("jsr-edit").style.display    = "none";
    document.getElementById("jsr-display").style.display = "block";
  });

  document.getElementById("jsr-input").addEventListener("keydown", function(e) {
    if (e.key === "Enter")  document.getElementById("jsr-confirm-btn").click();
    if (e.key === "Escape") document.getElementById("jsr-cancel-btn").click();
  });

  // Initial load — pass empty ZIP so JsRates uses getVisitorLocation() server-side
  fetchRates("");
})();
</script>

Security note: Since the API key is exposed in browser JavaScript, add your store's domain to the domain whitelist in JsRates Settings → API to prevent unauthorized use of your key from other origins.