diff --git a/app/favicon-pre57.ico b/app/favicon-pre57.ico new file mode 100644 index 000000000..ae5084bc0 Binary files /dev/null and b/app/favicon-pre57.ico differ diff --git a/app/favicon.ico b/app/favicon.ico index ae5084bc0..3d7ffcd86 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/images/firefox-logo.svg b/app/images/firefox-logo.svg new file mode 100644 index 000000000..d494a8717 --- /dev/null +++ b/app/images/firefox-logo.svg @@ -0,0 +1 @@ +firefox-logo \ No newline at end of file diff --git a/app/scripts/head/startup-styles.js b/app/scripts/head/startup-styles.js index 38b4203b4..5e0d0aba9 100644 --- a/app/scripts/head/startup-styles.js +++ b/app/scripts/head/startup-styles.js @@ -83,6 +83,7 @@ this.addSearchParamStyles(); this.addFxiOSSyncStyles(); this.addGetUserMediaStyles(); + this.addFx57Styles(); }, addJSStyle: function () { @@ -142,6 +143,15 @@ } else { this._addClass('no-getusermedia'); } + }, + + /** + * Add the `fx-57` class to the body if in Fx >= 57. + */ + addFx57Styles: function () { + if (this.environment.isFx57OrAbove() || this.environment.isFxiOS10OrAbove()) { + this._addClass('fx-57'); + } } }; diff --git a/app/scripts/lib/environment.js b/app/scripts/lib/environment.js index b85e593d2..0a2bf1765 100644 --- a/app/scripts/lib/environment.js +++ b/app/scripts/lib/environment.js @@ -108,6 +108,40 @@ return /FxiOS/.test(this.window.navigator.userAgent); }, + /** + * Is the user in Firefox for iOS 10 or above? + * + * @param {String} [userAgent=navigator.userAgent] UA string + * @returns {Boolean} + */ + isFxiOS10OrAbove: function (userAgent) { + var fxRegExp = /FxiOS\/(\d{2,}\.\d{1,})/; + var matches = fxRegExp.exec(userAgent || this.window.navigator.userAgent); + + if (matches && matches[1]) { + return parseFloat(matches[1]) >= 10; + } + + return false; + }, + + /** + * Is the user in Firefox (Desktop or Android) 57 or above? + * + * @param {String} [userAgent=navigator.userAgent] UA string + * @returns {Boolean} + */ + isFx57OrAbove: function (userAgent) { + var fxRegExp = /Firefox\/(\d{2,}\.\d{1,})$/; + var matches = fxRegExp.exec(userAgent || this.window.navigator.userAgent); + + if (matches && matches[1]) { + return parseFloat(matches[1]) >= 57; + } + + return false; + }, + hasSendBeacon: function () { return typeof this.window.navigator.sendBeacon === 'function'; } diff --git a/app/styles/modules/_branding.scss b/app/styles/modules/_branding.scss index 68e15d63c..9a4fbf6cd 100644 --- a/app/styles/modules/_branding.scss +++ b/app/styles/modules/_branding.scss @@ -26,6 +26,14 @@ display: none; } + html.fx-57 & { + background-image: image-url('firefox-logo.svg'); + background-position-y: -5px; + @include respond-to('small') { + background-position-y: -4px; + } + } + .static & { opacity: 1; } diff --git a/app/styles/modules/_settings.scss b/app/styles/modules/_settings.scss index 7ac19f599..3d954d081 100644 --- a/app/styles/modules/_settings.scss +++ b/app/styles/modules/_settings.scss @@ -40,6 +40,14 @@ body.settings #main-content.card { background-repeat: no-repeat; margin: 0; + html.fx-57 & { + background-image: image-url('firefox-logo.svg'); + background-position-y: -5px; + @include respond-to('small') { + background-position-y: -4px; + } + } + html[dir='ltr'] & { float: left; } @@ -78,7 +86,6 @@ body.settings #main-content.card { background-position: right 8px; padding-right: 36px; } - } } diff --git a/app/tests/spec/head/startup-styles.js b/app/tests/spec/head/startup-styles.js index 39f990b25..ec24cf058 100644 --- a/app/tests/spec/head/startup-styles.js +++ b/app/tests/spec/head/startup-styles.js @@ -178,6 +178,40 @@ define(function (require, exports, module) { }); }); + describe('addFx57Styles', () => { + it('adds `fx-57` if UA is Fx >= 57', () => { + sinon.stub(environment, 'isFx57OrAbove').callsFake(() => true); + sinon.stub(environment, 'isFxiOS10OrAbove').callsFake(() => false); + + startupStyles.addFx57Styles(); + assert.isTrue(/fx-57/.test(startupStyles.getClassName())); + }); + + it('does not add `fx-57` if UA is Fx <= 56', () => { + sinon.stub(environment, 'isFx57OrAbove').callsFake(() => false); + sinon.stub(environment, 'isFxiOS10OrAbove').callsFake(() => false); + + startupStyles.addFx57Styles(); + assert.isFalse(/fx-57/.test(startupStyles.getClassName())); + }); + + it('adds `fx-57` if UA is FxiOS >= 10', () => { + sinon.stub(environment, 'isFx57OrAbove').callsFake(() => false); + sinon.stub(environment, 'isFxiOS10OrAbove').callsFake(() => true); + + startupStyles.addFx57Styles(); + assert.isTrue(/fx-57/.test(startupStyles.getClassName())); + }); + + it('does not add `fx-57` if UA is FxiOS <= 9', () => { + sinon.stub(environment, 'isFx57OrAbove').callsFake(() => false); + sinon.stub(environment, 'isFxiOS10OrAbove').callsFake(() => false); + + startupStyles.addFx57Styles(); + assert.isFalse(/fx-57/.test(startupStyles.getClassName())); + }); + + }); describe('initialize', function () { it('runs all the tests', function () { diff --git a/app/tests/spec/lib/environment.js b/app/tests/spec/lib/environment.js index 97d8c75b0..574134f28 100644 --- a/app/tests/spec/lib/environment.js +++ b/app/tests/spec/lib/environment.js @@ -159,6 +159,40 @@ define(function (require, exports, module) { }); }); + describe('isFxiOS10OrAbove', function () { + it('returns `false` if on Fx 10 or above, false otw', function () { + assert.isFalse(environment.isFxiOS10OrAbove('FxiOS/9.0')); + assert.isTrue(environment.isFxiOS10OrAbove('FxiOS/10.0')); + assert.isTrue(environment.isFxiOS10OrAbove('FxiOS/11.0')); + assert.isFalse(environment.isFxiOS('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:40.0) Gecko/20100101 Firefox/40.0')); + }); + }); + + describe('isFx57OrAbove', () => { + it('returns `true` if Fx Desktop 57 or above', () => { + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:57.0) Gecko/20100101 Firefox/57.0')); + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:58.0) Gecko/20100101 Firefox/58.0')); + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:101.0) Gecko/20100101 Firefox/101.0')); + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Windows NT x.y; WOW64; rv:101.0) Gecko/20100101 Firefox/101.0')); + }); + + it('returns `true` if Fx for Android 57 or above', () => { + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Android 4.4; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0')); + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Android 4.4; Mobile; rv:57.0) Gecko/58.0 Firefox/58.0')); + assert.isTrue(environment.isFx57OrAbove('Mozilla/5.0 (Android 4.4; Mobile; rv:57.0) Gecko/999.0 Firefox/999.0')); + }); + + it('returns `false` otw', () => { + assert.isFalse(environment.isFx57OrAbove('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:40.0) Gecko/20100101 Firefox/40.0')); + assert.isFalse(environment.isFx57OrAbove('Mozilla/5.0 (Windows NT x.y; WOW64; rv:10.0) Gecko/20100101 Firefox/10.0')); + assert.isFalse(environment.isFx57OrAbove('Mozilla/5.0 (Android; Mobile; rv:40.0) Gecko/40.0 Firefox/40.0')); + assert.isFalse(environment.isFx57OrAbove('Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0')); + assert.isFalse(environment.isFx57OrAbove('Mozilla/5.0 (Android 4.4; Mobile; rv:56.0) Gecko/56.0 Firefox/56.0')); + assert.isFalse(environment.isFx57OrAbove( + 'Mozilla/5.0 (iPod touch; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4')); + }); + }); + describe('hasSendBeacon', function () { it('returns `true` if sendBeacon function exists', function () { windowMock.navigator.sendBeacon = function () {}; diff --git a/server/lib/routes.js b/server/lib/routes.js index 1e4f55566..ef426e63a 100644 --- a/server/lib/routes.js +++ b/server/lib/routes.js @@ -32,6 +32,7 @@ module.exports = function (config, i18n) { // Disable server verification for now due to issues with customs //require('./routes/get-verify-email')(), require('./routes/get-apple-app-site-association')(), + require('./routes/get-favicon')(), require('./routes/get-frontend')(), require('./routes/get-terms-privacy')(i18n), require('./routes/get-index')(config), diff --git a/server/lib/routes/get-favicon.js b/server/lib/routes/get-favicon.js new file mode 100644 index 000000000..df96745b7 --- /dev/null +++ b/server/lib/routes/get-favicon.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Serve up the favicon. A temporary route to serve the Fx 57 logo to + * only Fx >= 57 until the logo is publicly released. + */ + +const uaParser = require('node-uap'); + +function shouldSee57Icon(uaHeader) { + const agent = uaParser.parse(uaHeader); + return (agent.ua.family === 'Firefox' && parseInt(agent.ua.major, 10) >= 57) || + (agent.ua.family === 'Firefox Mobile' && parseInt(agent.ua.major, 10) >= 57) || + (agent.ua.family === 'Firefox iOS' && parseInt(agent.ua.major, 10) >= 10); +} + +module.exports = function () { + return { + method: 'get', + path: '/favicon.ico', + process: (req, res, next) => { + if (! shouldSee57Icon(req.headers['user-agent'] || '')) { + req.url = 'favicon-pre57.ico'; + } + next(); + } + }; +}; diff --git a/tests/functional/lib/ua-strings.js b/tests/functional/lib/ua-strings.js index f7dfadc5d..f5a89f546 100644 --- a/tests/functional/lib/ua-strings.js +++ b/tests/functional/lib/ua-strings.js @@ -7,6 +7,8 @@ define([], function () { return { 'android_chrome': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19', 'android_firefox': 'Mozilla/5.0 (Android 4.4; Mobile; rv:43.0) Gecko/41.0 Firefox/43.0', + 'android_firefox_56': 'Mozilla/5.0 (Android 4.4; Mobile; rv:56.0) Gecko/56.0 Firefox/56.0', + 'android_firefox_57': 'Mozilla/5.0 (Android 4.4; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0', 'desktop_chrome': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.59 Safari/537.36', 'desktop_firefox': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:50.0) Gecko/20100101 Firefox/50.0', 'desktop_firefox_55': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0', @@ -16,6 +18,8 @@ define([], function () { 'ios_firefox': 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4', 'ios_firefox_6_0': 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/6.0 Mobile/12F69 Safari/600.1.4', 'ios_firefox_6_1': 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/6.1 Mobile/12F69 Safari/600.1.4', + 'ios_firefox_9': 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/9.0 Mobile/12F69 Safari/600.1.4', + 'ios_firefox_10': 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/10.0 Mobile/12F69 Safari/600.1.4', // eslint-disable-line 'ios_safari': 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3' }; /*eslint-enable max-len*/ diff --git a/tests/intern_server.js b/tests/intern_server.js index bdfaca103..5e6c782fc 100644 --- a/tests/intern_server.js +++ b/tests/intern_server.js @@ -33,6 +33,7 @@ define([ 'tests/server/statsd-collector', 'tests/server/raven', 'tests/server/routes/get-apple-app-site-association', + 'tests/server/routes/get-favicon', 'tests/server/routes/get-config', 'tests/server/routes/get-verify-email', 'tests/server/routes/get-fxa-client-configuration', diff --git a/tests/server/routes/get-favicon.js b/tests/server/routes/get-favicon.js new file mode 100644 index 000000000..c3961a534 --- /dev/null +++ b/tests/server/routes/get-favicon.js @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +define([ + 'intern!object', + 'intern/chai!assert', + 'intern/dojo/node!../../../server/lib/routes/get-favicon', + '../../functional/lib/ua-strings', +], function (registerSuite, assert, route, uaStrings) { + let instance, request, response; + + /*eslint-disable sorting/sort-object-props*/ + registerSuite({ + name: 'routes/get-favicon', + + 'route interface is correct': function () { + assert.isFunction(route); + assert.lengthOf(route, 0); + }, + + 'initialise route': { + setup: function () { + instance = route(); + }, + + 'instance interface is correct': function () { + assert.isObject(instance); + assert.lengthOf(Object.keys(instance), 3); + assert.equal(instance.method, 'get'); + assert.equal(instance.path, '/favicon.ico'); + assert.isFunction(instance.process); + assert.lengthOf(instance.process, 3); + }, + + 'route.process': { + 'no user-agent header': { + setup: function () { + request = { + headers: {}, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see old icon': function () { + assert.equal(request.url, 'favicon-pre57.ico'); + } + }, + + 'Firefox desktop 56': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.desktop_firefox_56 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see old icon': function () { + assert.equal(request.url, 'favicon-pre57.ico'); + } + }, + + 'Firefox desktop 57': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.desktop_firefox_57 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see new icon': function () { + assert.equal(request.url, 'favicon.ico'); + } + }, + + 'Firefox android 56': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.android_firefox_56 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see old icon': function () { + assert.equal(request.url, 'favicon-pre57.ico'); + } + }, + + 'Firefox android 57': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.android_firefox_57 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see new icon': function () { + assert.equal(request.url, 'favicon.ico'); + } + }, + + 'Firefox iOS 9': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.ios_firefox_9 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see old icon': function () { + assert.equal(request.url, 'favicon-pre57.ico'); + } + }, + + 'Firefox iOS 10': { + setup: function () { + request = { + headers: { + 'user-agent': uaStrings.ios_firefox_10 + }, + url: 'favicon.ico' + }; + return new Promise((resolve) => { + instance.process(request, response, resolve); + }); + }, + + 'should see new icon': function () { + assert.equal(request.url, 'favicon.ico'); + } + }, + } + } + }); + /*eslint-enable sorting/sort-object-props*/ +});