All our sites should contain this smartbanner script by default, but just in case you come across a site that doesn’t have it, here’s all the pieces and how to add it.

How To

Script - Plugin File

This smartbanner setup is what we currently use, and it’s also archived below just in case the original link ever goes down. This should go within the scripts > plugins folder.

/*!
 * jQuery Smart Banner
 * Copyright (c) 2012 Arnold Daniels <arnold@jasny.net>
 * Based on 'jQuery Smart Web App Banner' by Kurt Zenisek @ kzeni.com
 */
(function(root, factory) {
  if (typeof define == 'function' && define.amd) {
    define(['jquery'], factory);
  } else {
    factory(root.jQuery);
  }
})(this, function($) {
  var UA = navigator.userAgent;
  var isEdge = /Edge/i.test(UA);

  var SmartBanner = function(options) {
    // Get the original margin-top of the HTML element so we can take that into account.
    this.origHtmlMargin = parseFloat($('html').css('margin-top'));
    this.options = $.extend({}, $.smartbanner.defaults, options);

    // Check if it's already a standalone web app or running within a webui view of an app (not mobile safari).
    var standalone = navigator.standalone;

    // Detect banner type (iOS or Android).
    if (this.options.force) {
      this.type = this.options.force;
    }
    else if (UA.match(/Windows Phone/i) !== null && UA.match(/Edge|Touch/i) !== null) {
      this.type = 'windows';
    }
    else if (UA.match(/iPhone|iPod/i) !== null || (UA.match(/iPad/) && this.options.iOSUniversalApp)) {
      if (UA.match(/Safari/i) !== null &&
          (UA.match(/CriOS/i) !== null ||
           UA.match(/FxiOS/i) != null ||
            window.Number(UA.substr(UA.indexOf('OS ') + 3, 3).replace('_', '.')) < 6)) {
        // Check webview and native smart banner support (iOS 6+).
        this.type = 'ios';
      }
    }
    else if (UA.match(/\bSilk\/(.*\bMobile Safari\b)?/) || UA.match(/\bKF\w/) || UA.match('Kindle Fire')) {
      this.type = 'kindle';
    }
    else if (UA.match(/Android/i) !== null) {
      this.type = 'android';
    }
    // Don't show banner if device isn't iOS or Android, website is loaded in app or user dismissed banner.
    if (!this.type || standalone || this.getCookie('sb-closed') || this.getCookie('sb-installed')) {
      return;
    }
    // Calculate scale.
    this.scale = this.options.scale == 'auto' ? $(window).width() / window.screen.width : this.options.scale;
    if (this.scale < 1) {
      this.scale = 1;
    }
    // Get info from meta data.
    var meta = $(
      this.type == 'android'
        ? 'meta[name="google-play-app"]'
        : (this.type == 'ios'
            ? 'meta[name="apple-itunes-app"]'
            : (this.type == 'kindle'
                ? 'meta[name="kindle-fire-app"]'
                : 'meta[name="msApplication-ID"]'
              )
          )
    );

    if (!meta.length) {
      return;
    }
    // For Windows Store apps, get the PackageFamilyName for protocol launch.
    if (this.type == 'windows') {
      if (isEdge) {
        this.appId = $('meta[name="msApplication-PackageEdgeName"]').attr('content');
      }
      if (!this.appId) {
        this.appId = $('meta[name="msApplication-PackageFamilyName"]').attr('content');
      }
    }
    else {
      // Try to pull the appId out of the meta tag and store the result.
      var parsedMetaContent = /app-id=([^\s,]+)/.exec(meta.attr('content'));
      if (parsedMetaContent) {
        this.appId = parsedMetaContent[1];
      } else {
        return;
      }
    }
    this.title = this.options.title
      ? this.options.title
      : (meta.data('title') || $('title').text().replace(/\s*[|\-ยท].*$/, ''));

    this.author = this.options.author
      ? this.options.author
      : (meta.data('author') || ($('meta[name="author"]').length ? $('meta[name="author"]').attr('content') : window.location.hostname));

    this.iconUrl = meta.data('icon-url');
    this.price = meta.data('price');

    // Set default onInstall callback if not set in options.
    if (typeof this.options.onInstall == 'function') {
      this.options.onInstall = this.options.onInstall;
    } else {
      this.options.onInstall = function() {};
    }
    // Set default onClose callback if not set in options.
    if (typeof this.options.onClose == 'function') {
      this.options.onClose = this.options.onClose;
    } else {
      this.options.onClose = function() {};
    }
    // Create banner.
    this.create();
    this.show();
    this.listen();
  };

  SmartBanner.prototype = {

    constructor: SmartBanner,

    create: function() {
      var iconURL;
      var price = this.price || this.options.price;

      var link = this.options.url || (function() {
        switch (this.type) {
          case 'android':
            return 'market://details?id=';
          case 'kindle':
            return 'amzn://apps/android?asin=';
          case 'windows':
            return isEdge
              ? 'ms-windows-store://pdp/?productid='
              : 'ms-windows-store:navigate?appid=';
        }
        return 'https://itunes.apple.com/' + this.options.appStoreLanguage + '/app/id';
      }.call(this) + this.appId);

      var inStore = !price ? '' : (function() {
        var result = price + ' - ';
        switch (this.type) {
          case 'android':
            return result + this.options.inGooglePlay;
          case 'kindle':
            return result + this.options.inAmazonAppStore;
          case 'windows':
            return result + this.options.inWindowsStore;
        }
        return result + this.options.inAppStore
      }.call(this));

      var gloss = this.options.iconGloss == null
        ? (this.type=='ios')
        : this.options.iconGloss;

      if (this.type == 'android' && this.options.GooglePlayParams) {
        link += '&referrer=' + this.options.GooglePlayParams;
      }
      var banner = (
        '<div id="smartbanner" class="' + this.type + '">' +
          '<div class="sb-container">' +
            '<a href="#" class="sb-close">&times;</a>' +
            '<span class="sb-icon"></span>' +
            '<div class="sb-info">' +
              '<strong>' + this.title + '</strong>' +
              '<span>' + this.author + '</span>' +
              '<span>' + inStore + '</span>' +
            '</div>' +
            '<a href="' + link + '" class="sb-button">' +
              '<span>' + this.options.button + '</span>' +
            '</a>' +
          '</div>' +
        '</div>'
      );
      if (this.options.layer) {
        $(this.options.appendToSelector).append(banner);
      } else {
        $(this.options.appendToSelector).prepend(banner);
      }
      if (this.options.icon) {
        iconURL = this.options.icon;
      } else if(this.iconUrl) {
        iconURL = this.iconUrl;
      } else if ($('link[rel="apple-touch-icon-precomposed"]').length > 0) {
        iconURL = $('link[rel="apple-touch-icon-precomposed"]').attr('href');
        if (this.options.iconGloss == null) {
          gloss = false;
        }
      } else if ($('link[rel="apple-touch-icon"]').length > 0) {
        iconURL = $('link[rel="apple-touch-icon"]').attr('href');
      } else if ($('meta[name="msApplication-TileImage"]').length > 0) {
        iconURL = $('meta[name="msApplication-TileImage"]').attr('content');
      } else if ($('meta[name="msapplication-TileImage"]').length > 0) {
        // Redundant because ms docs show two case usages.
        iconURL = $('meta[name="msapplication-TileImage"]').attr('content');
      }
      if (iconURL) {
        $('#smartbanner .sb-icon').css('background-image', 'url(' + iconURL + ')');
        if (gloss) {
          $('#smartbanner .sb-icon').addClass('gloss');
        }
      } else{
        $('#smartbanner').addClass('no-icon');
      }
      this.bannerHeight = $('#smartbanner').outerHeight() + 2;

      if (this.scale > 1) {
        $('#smartbanner')
          .css('top', parseFloat($('#smartbanner').css('top')) * this.scale)
          .css('height', parseFloat($('#smartbanner').css('height')) * this.scale)
          .hide();
        $('#smartbanner .sb-container')
          .css('-webkit-transform', 'scale(' + this.scale + ')')
          .css('-msie-transform', 'scale(' + this.scale + ')')
          .css('-moz-transform', 'scale(' + this.scale + ')')
          .css('width', $(window).width() / this.scale);
      }
      $('#smartbanner')
        .css('position', this.options.layer ? 'absolute' : 'static');
    },

    listen: function() {
      $('#smartbanner .sb-close').on('click', $.proxy(this.close, this));
      $('#smartbanner .sb-button').on('click', $.proxy(this.install, this));
    },

    show: function(callback) {
      var banner = $('#smartbanner');
      banner.stop();

      if (this.options.layer) {
        banner
          .animate({ top: 0, display: 'block' }, this.options.speedIn)
          .addClass('shown')
          .show();
        $(this.pushSelector)
          .animate({
            paddingTop: this.origHtmlMargin + (this.bannerHeight * this.scale)
          }, this.options.speedIn, 'swing', callback);
      }
      else {
        if ($.support.transition) {
          banner.animate({ top: 0 }, this.options.speedIn).addClass('shown');
          var transitionCallback = function() {
            $('html').removeClass('sb-animation');
            if (callback) {
              callback();
            }
          };
          $(this.pushSelector)
            .addClass('sb-animation')
            .one($.support.transition.end, transitionCallback)
            .emulateTransitionEnd(this.options.speedIn)
            .css('margin-top', this.origHtmlMargin + (this.bannerHeight * this.scale));
        }
        else {
          banner
            .slideDown(this.options.speedIn)
            .addClass('shown');
        }
      }
    },

    hide: function(callback) {
      var banner = $('#smartbanner');
      banner.stop();

      if (this.options.layer) {
        banner.animate({
          top: -1 * this.bannerHeight * this.scale,
          display: 'block'
        }, this.options.speedIn)
        .removeClass('shown');

        $(this.pushSelector)
          .animate({
            paddingTop: this.origHtmlMargin
          }, this.options.speedIn, 'swing', callback);
      }
      else {
        if ($.support.transition) {
          if (this.type !== 'android') {
            banner
              .css('top', -1 * this.bannerHeight * this.scale)
              .removeClass('shown');
          }
          else {
            banner
              .css({display:'none'})
              .removeClass('shown');
          }
          var transitionCallback = function() {
            $('html').removeClass('sb-animation');
            if (callback) {
              callback();
            }
          };
          $(this.pushSelector)
            .addClass('sb-animation')
            .one($.support.transition.end, transitionCallback)
            .emulateTransitionEnd(this.options.speedOut)
            .css('margin-top', this.origHtmlMargin);
        }
        else {
          banner.slideUp(this.options.speedOut).removeClass('shown');
        }
      }
    },

    close: function(e) {
      e.preventDefault();
      this.hide();
      this.setCookie('sb-closed', 'true', this.options.daysHidden);
      this.options.onClose(e);
    },

    install: function(e) {
      if (this.options.hideOnInstall) {
        this.hide();
      }
      this.setCookie('sb-installed', 'true', this.options.daysReminder);
      this.options.onInstall(e);
    },

    setCookie: function(name, value, exdays) {
      var exdate = new Date();
      exdate.setDate(exdate.getDate() + exdays);
      value = encodeURI(value) + ((exdays == null) ? '' : '; expires=' + exdate.toUTCString());
      document.cookie = name + '=' + value + '; path=/;';
    },

    getCookie: function(name) {
      var i, x, y, ARRcookies = document.cookie.split(';');
      for (i = 0; i < ARRcookies.length; i++) {
        x = ARRcookies[i].substr(0, ARRcookies[i].indexOf('='));
        y = ARRcookies[i].substr(ARRcookies[i].indexOf('=') + 1);
        x = x.replace(/^\s+|\s+$/g, '');
        if (x == name) {
          return decodeURI(y);
        }
      }
      return null;
    },

    // Demo only.
    switchType: function() {
      var that = this;

      this.hide(function() {
        that.type = that.type == 'android' ? 'ios' : 'android';
        var meta = $(that.type == 'android' ? 'meta[name="google-play-app"]' : 'meta[name="apple-itunes-app"]').attr('content');
        that.appId = /app-id=([^\s,]+)/.exec(meta)[1];

        $('#smartbanner').detach();
        that.create();
        that.show();
      });
    }
  };

  $.smartbanner = function(option) {
    var $window = $(window);
    var data = $window.data('smartbanner');
    var options = typeof option == 'object' && option;
    if (!data) {
      $window.data('smartbanner', (data = new SmartBanner(options)));
    }
    if (typeof option == 'string') {
      data[option]();
    }
  };

  // override these globally if you like (they are all optional)
  $.smartbanner.defaults = {
    title: null, // What the title of the app should be in the banner (defaults to <title>)
    author: null, // What the author of the app should be in the banner (defaults to <meta name="author"> or hostname)
    price: 'FREE', // Price of the app
    appStoreLanguage: 'us', // Language code for App Store
    inAppStore: 'On the App Store', // Text of price for iOS
    inGooglePlay: 'In Google Play', // Text of price for Android
    inAmazonAppStore: 'In the Amazon Appstore',
    inWindowsStore: 'In the Windows Store', //Text of price for Windows
    GooglePlayParams: null, // Aditional parameters for the market
    icon: null, // The URL of the icon (defaults to <meta name="apple-touch-icon">)
    iconGloss: null, // Force gloss effect for iOS even for precomposed
    button: 'VIEW', // Text for the install button
    url: null, // The URL for the button. Keep null if you want the button to link to the app store.
    scale: 'auto', // Scale based on viewport size (set to 1 to disable)
    speedIn: 300, // Show animation speed of the banner
    speedOut: 400, // Close animation speed of the banner
    daysHidden: 15, // Duration to hide the banner after being closed (0 = always show banner)
    daysReminder: 90, // Duration to hide the banner after "VIEW" is clicked *separate from when the close button is clicked* (0 = always show banner)
    force: null, // Choose 'ios', 'android' or 'windows'. Don't do a browser check, just always show this banner
    hideOnInstall: true, // Hide the banner after "VIEW" is clicked.
    layer: false, // Display as overlay layer or slide down the page
    iOSUniversalApp: true, // If the iOS App is a universal app for both iPad and iPhone, display Smart Banner to iPad users, too.
    appendToSelector: 'body', //Append the banner to a specific selector
    pushSelector: 'html' // What element is going to push the site content down; this is where the banner append animation will start.
  };

  $.smartbanner.Constructor = SmartBanner;

  // ============================================================
  // Bootstrap transition
  // Copyright 2011-2014 Twitter, Inc.
  // Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)

  function transitionEnd () {
    var el = document.createElement('smartbanner');

    var transEndEventNames = {
      WebkitTransition: 'webkitTransitionEnd',
      MozTransition: 'transitionend',
      OTransition: 'oTransitionEnd otransitionend',
      transition: 'transitionend'
    };

    for (var name in transEndEventNames) {
      if (el.style[name] !== undefined) {
        return {end: transEndEventNames[name]};
      }
    }
    // Explicit for ie8.
    return false;
  }
  if ($.support.transition !== undefined) {
    // Prevent conflict with Twitter Bootstrap.
    return;
  }

  // http://blog.alexmaccaw.com/css-transitions
  $.fn.emulateTransitionEnd = function(duration) {
    var called = false, $el = this;
    $(this).one($.support.transition.end, function() {
      called = true;
    });
    var callback = function() {
      if (!called) {
        $($el).trigger($.support.transition.end);
      }
    };
    setTimeout(callback, duration);
    return this;
  };

  $(function() {
    $.support.transition = transitionEnd();
  });
  // ============================================================
});

Styles

I usually prefer to put the CSS within src > scss > custom > plugins > jquery.smartbanner.scss

Sass File

.smartbanner {
  position: absolute;
  top: 0;
  left: 0;
  overflow-x: hidden;
  width: 100%;
  height: 84px;
  background: $gray-100;
  font-family: $font-family-base;
}
.smartbanner__exit {
  position: absolute;
  top: calc(50% - 6px);
  left: 9px;
  display: block;
  margin: 0;
  width: 12px;
  height: 12px;
  border: 0;
  text-align: center;
}
.smartbanner__exit::before, .smartbanner__exit::after {
  position: absolute;
  width: 1px;
  height: 12px;
  background: $gray-600;
  content: ' ';
}
.smartbanner__exit::before {
  -webkit-transform: rotate(45deg);
          transform: rotate(45deg);
}
.smartbanner__exit::after {
  -webkit-transform: rotate(-45deg);
          transform: rotate(-45deg);
}
.smartbanner__icon {
  position: absolute;
  top: 10px;
  left: 30px;
  width: 64px;
  height: 64px;
  border-radius: 15px;
  background-size: 64px 64px;
}
.smartbanner__info {
  position: absolute;
  top: 10px;
  left: 104px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  overflow-y: hidden;
  width: 60%;
  height: 64px;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
  }
.smartbanner__info__title {
  font-size: $font-size-base - 2;
}
.smartbanner__info__author, .smartbanner__info__price {
  font-size: $font-size-base - 4;
}
.smartbanner__button {
  position: absolute;
  top: 32px;
  right: 10px;
  z-index: 1;
  display: block;
  padding: 0 10px;
  min-width: 10%;
  border-radius: $btn-border-radius;
  background: $primary;
  color: yiq-color($primary);
  font-size: $font-size-base + 2 ;
  text-align: center;
  text-decoration: none;
}
.smartbanner__button__label {
  text-align: center;
}

HTML - smartbanner.mustache

I create a separate mustache file for the smartbanner HTML pieces, and include it within top.mustache and top-404.mustache. Be sure to update the FI name in both places, and both store URLs within the meta tags!

<meta name="smartbanner:title" content="FINAME Mobile App">
<meta name="smartbanner:author" content="FINAME">
<meta name="smartbanner:price" content="FREE">
<meta name="smartbanner:price-suffix-apple" content=" - On the App Store">
<meta name="smartbanner:price-suffix-google" content=" - In Google Play">
<meta name="smartbanner:icon-apple" content="/apple-touch-icon.png">
<meta name="smartbanner:icon-google" content="/apple-touch-icon.png">
<meta name="smartbanner:button" content="VIEW">
<meta name="smartbanner:button-url-apple" content="FIAPPLEURL">
<meta name="smartbanner:button-url-google" content="FIGOOGLEPLAYURL">
<meta name="smartbanner:enabled-platforms" content="android,ios">

Example Sites

Tags: bs2, bs3, bs4, smartbanner, js, html, scss, sass, less, javascript, jquery, script, plugin