├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .sass-lint.yml ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── .eslintrc ├── env.js ├── jest │ ├── cssTransform.js │ ├── fileTransform.js │ ├── setup.js │ ├── setupTestFramework.js │ └── transform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── example ├── icon.png └── index.html ├── package.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── components │ ├── SmartBanner.js │ └── __tests__ │ │ ├── SmartBanner.test.js │ │ └── __snapshots__ │ │ └── SmartBanner.test.js.snap ├── example.js ├── icon.png ├── index.html └── styles │ └── style.scss └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: qEafeG1Qpc3JMNK787kjVwNUxqtyoXlWl 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | example/* 3 | test/* 4 | node_modules/* 5 | package.json 6 | webpack.*.js 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-airbnb', 'prettier'], 3 | parser: 'babel-eslint', 4 | env: { 5 | browser: true, 6 | node: true, 7 | es6: true, 8 | jest: true, 9 | }, 10 | rules: { 11 | 'camelcase': ["error", { allow: ["^UNSAFE_"]}], 12 | 'react/sort-comp': [1, { 13 | order: [ 14 | 'static-methods', 15 | 'lifecycle', 16 | '/^UNSAFE_/', 17 | 'everything-else' 18 | ], 19 | }], 20 | 'prefer-destructuring': 'off', 21 | 'jsx-a11y/click-events-have-key-events': 'off', 22 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 23 | 'jsx-a11y/no-autofocus': 'off', 24 | 'jsx-a11y/no-noninteractive-tabindex': 'off', 25 | 'jsx-a11y/anchor-has-content': 'off', 26 | 'react/destructuring-assignment': 'off', 27 | 'react/jsx-no-bind': 'error', 28 | 'react/no-multi-comp': 'off', 29 | 'no-restricted-syntax': [ 30 | 'error', 31 | 'DebuggerStatement', 32 | 'ForInStatement', 33 | 'WithStatement', 34 | ], 35 | 'newline-after-var': ['error', 'always'], 36 | 'newline-before-return': 'error', 37 | 'comma-dangle': ['error', 'always-multiline'], // https://github.com/airbnb/javascript/commit/788208295469e19b806c06e01095dc8ba1b6cdc9 38 | indent: ['error', 2, { SwitchCase: 1 }], 39 | 'no-console': 0, 40 | 'no-alert': 0, 41 | 'no-underscore-dangle': 'off', 42 | 'max-len': ['error', 150, 2, { ignoreUrls: true, ignoreComments: false }], 43 | 'react/require-default-props': 'off', 44 | 'react/jsx-curly-spacing': 'off', 45 | 'arrow-body-style': 'off', 46 | 'no-mixed-operators': [ 47 | 'error', 48 | { 49 | groups: [ 50 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 51 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 52 | ['&&', '||'], 53 | ['in', 'instanceof'], 54 | ], 55 | allowSamePrecedence: true, 56 | }, 57 | ], 58 | 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx'] }], 59 | 'react/no-string-refs': 'off', 60 | 'arrow-parens': 'off', 61 | 'jsx-a11y/no-static-element-interactions': 'off', 62 | 'react/prefer-stateless-function': 'off', 63 | 'no-param-reassign': 'off', 64 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 65 | 'import/no-unresolved': [ 66 | 2, 67 | { ignore: ['react', 'react-dom', 'react-intl-tel-input'] }, 68 | ], 69 | 'import/extensions': 'off', 70 | 'import/no-extraneous-dependencies': [ 71 | 'error', 72 | { 73 | devDependencies: [ 74 | 'test/**', // tape, common npm pattern 75 | 'tests/**', // also common npm pattern 76 | 'spec/**', // mocha, rspec-like pattern 77 | '**/__tests__/**', // jest pattern 78 | '**/__mocks__/**', // jest pattern 79 | 'test.js', // repos with a single test file 80 | 'test-*.js', // repos with multiple top-level test files 81 | '**/*.test.js', // tests where the extension denotes that it is a test 82 | '**/webpack.config.js', // webpack config 83 | '**/webpack.config.*.js', // webpack config 84 | 'config/jest/**', 85 | 'src/testUtils/**', 86 | '*.js', 87 | ], 88 | optionalDependencies: false, 89 | }, 90 | ], 91 | indent: [ 92 | 'error', 93 | 2, 94 | { 95 | SwitchCase: 1, 96 | VariableDeclarator: 1, 97 | outerIIFEBody: 1, 98 | MemberExpression: 1, 99 | // CallExpression: { 100 | // parameters: null, 101 | // }, 102 | FunctionDeclaration: { 103 | parameters: 1, 104 | body: 1, 105 | }, 106 | FunctionExpression: { 107 | parameters: 1, 108 | body: 1, 109 | }, 110 | }, 111 | ], 112 | 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], 113 | }, 114 | plugins: ['react', 'import', 'security'], 115 | globals: { 116 | __DEVELOPMENT__: true, 117 | __CLIENT__: true, 118 | __SERVER__: true, 119 | __DISABLE_SSR__: true, 120 | __DEVTOOLS__: true, 121 | }, 122 | }; 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SublimeText ### 2 | *.sublime-workspace 3 | 4 | ### JetBrains ### 5 | .idea 6 | 7 | ### OSX ### 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Visual Studio 17 | *.history 18 | *.vscode 19 | .vscode/ 20 | .history/ 21 | 22 | # Files that might appear on external disk 23 | .Spotlight-V100 24 | .Trashes 25 | 26 | ### Windows ### 27 | # Windows image file caches 28 | Thumbs.db 29 | ehthumbs.db 30 | 31 | # Folder config file 32 | Desktop.ini 33 | 34 | # Recycle Bin used on file shares 35 | $RECYCLE.BIN/ 36 | 37 | # App specific 38 | 39 | dist/ 40 | node_modules/ 41 | .tmp 42 | /src/main.js 43 | .grunt/ 44 | .history/ 45 | npm-debug.log 46 | build/** 47 | dist/** 48 | example/** 49 | test/** 50 | coverage/* 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist/* 2 | 3 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | merge-default-rules: false 3 | formatter: stylish 4 | files: 5 | include: '**/*.s+(a|c)ss' 6 | ignore: 7 | - 'node_modules/**/*.*' 8 | - 'src/styles/vendor/**/*.*' 9 | rules: 10 | # Extends 11 | extends-before-mixins: 0 12 | extends-before-declarations: 0 13 | placeholder-in-extend: 2 14 | 15 | # Mixins 16 | mixins-before-declarations: 2 17 | 18 | # Line Spacing 19 | one-declaration-per-line: 2 20 | empty-line-between-blocks: 2 21 | single-line-per-selector: 2 22 | 23 | # Disallows 24 | no-attribute-selectors: 0 25 | no-color-hex: 0 26 | no-color-keywords: 0 27 | no-combinators: 0 28 | no-css-comments: 0 29 | no-debug: 2 30 | no-disallowed-properties: 0 31 | no-duplicate-properties: 2 32 | no-empty-rulesets: 2 33 | no-extends: 0 34 | no-ids: 2 35 | no-important: 2 36 | no-invalid-hex: 2 37 | no-mergeable-selectors: 2 38 | no-misspelled-properties: 39 | - 2 40 | - extra-properties: ['overflow-scrolling'] 41 | no-qualifying-elements: 0 42 | no-trailing-whitespace: 2 43 | no-trailing-zero: 2 44 | no-transition-all: 2 45 | no-universal-selectors: 0 46 | no-url-protocols: 2 47 | no-vendor-prefixes: 0 48 | no-warn: 2 49 | property-units: 0 50 | 51 | # Nesting 52 | force-attribute-nesting: 2 53 | force-element-nesting: 2 54 | force-pseudo-nesting: 2 55 | 56 | # Name Formats 57 | class-name-format: 2 58 | function-name-format: 2 59 | id-name-format: 60 | - 2 61 | - convention: snakecase 62 | mixin-name-format: 2 63 | placeholder-name-format: 2 64 | variable-name-format: 2 65 | 66 | # Style Guide 67 | attribute-quotes: 2 68 | bem-depth: 0 69 | border-zero: 2 70 | brace-style: 2 71 | clean-import-paths: 2 72 | empty-args: 2 73 | hex-length: 2 74 | hex-notation: 2 75 | indentation: 2 76 | leading-zero: 77 | - 2 78 | - include: true 79 | nesting-depth: 0 80 | property-sort-order: 0 81 | pseudo-element: 0 82 | quotes: 83 | - 2 84 | - style: single 85 | shorthand-values: 2 86 | url-quotes: 2 87 | variable-for-property: 2 88 | zero-unit: 2 89 | 90 | # Inner Spacing 91 | space-after-comma: 2 92 | space-before-colon: 2 93 | space-after-colon: 2 94 | space-before-brace: 2 95 | space-before-bang: 2 96 | space-after-bang: 2 97 | space-between-parens: 2 98 | space-around-operator: 2 99 | 100 | # Final Items 101 | trailing-semicolon: 2 102 | final-newline: 2 103 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | yarn: true 4 | directories: 5 | - node_modules 6 | node_js: 7 | - "8.10.0" 8 | script: 9 | - yarn test 10 | - yarn lint 11 | - yarn build 12 | after_success: yarn run coverage 13 | after_script: yarn run coveralls 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Patrick Wang (patw) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-SmartBanner 2 | 3 | [![Build Status](https://travis-ci.org/patw0929/react-smartbanner.svg)](https://travis-ci.org/patw0929/react-smartbanner) 4 | [![npm version](https://badge.fury.io/js/react-smartbanner.svg)](http://badge.fury.io/js/react-smartbanner) 5 | [![Coverage Status](https://coveralls.io/repos/github/patw0929/react-smartbanner/badge.svg?branch=master)](https://coveralls.io/github/patw0929/react-smartbanner?branch=master) 6 | [![npm](https://img.shields.io/npm/l/express.svg?maxAge=2592000)]() 7 | 8 | Rewrite [Smart App Banner](https://github.com/kudago/smart-app-banner) in React.js. 9 | 10 | 11 | ## Demo & Examples 12 | 13 | Live demo: [patw0929.github.io/react-smartbanner](https://patw0929.github.io/react-smartbanner/) 14 | 15 | To build the examples locally, run: 16 | 17 | ```bash 18 | npm install 19 | npm start 20 | ``` 21 | 22 | or 23 | 24 | ```bash 25 | yarn 26 | yarn start 27 | ``` 28 | 29 | ## Installation 30 | 31 | The easiest way to use react-smartbanner is to install it from NPM and include it in your own React build process (using [Webpack](http://webpack.github.io/), etc). 32 | 33 | You can also use the standalone build by including `dist/main.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 34 | 35 | ``` 36 | npm install react-smartbanner --save 37 | ``` 38 | 39 | or 40 | 41 | ```bash 42 | yarn add react-smartbanner 43 | ``` 44 | 45 | ## Compatibility 46 | 47 | | react-smartbanner version | React version | 48 | | --- | --- | 49 | | `4.x.x+` | `^16.0.0` | 50 | | `3.x.x` | `^15.0.0` | 51 | 52 | 53 | ## Usage 54 | 55 | Remember to add following meta tags in your HTML page: (Use Facebook app as example) 56 | 57 | ```html 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | And React-SmartBanner component usage: 70 | 71 | ```javascript 72 | import React from 'react'; 73 | import ReactDOM from 'react-dom'; 74 | import SmartBanner from 'react-smartbanner'; 75 | import 'react-smartbanner/dist/main.css'; 76 | 77 | ReactDOM.render(, document.getElementById('content')); 78 | ``` 79 | 80 | ### Properties 81 | 82 | Please see the [Demo Page](https://patw0929.github.io/react-smartbanner/) 83 | 84 | 85 | ## Development (`src` and the build process) 86 | 87 | **NOTE:** The source code for the component is in `src`. A UMD bundle is also built to `dist`, which can be included without the need for any build system. 88 | 89 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. 90 | 91 | If you want to build to the bundle file to `dist/` folder, please run: 92 | 93 | ```bash 94 | npm run build 95 | ``` 96 | 97 | or 98 | 99 | ```bash 100 | yarn run build 101 | ``` 102 | 103 | ## Contributing 104 | 105 | To contribute to react-smartbanner, clone this repo locally and commit your code on a separate branch. Please write tests for your code, and run the linter before opening a pull-request: 106 | 107 | ```bash 108 | npm test 109 | npm run lint 110 | ``` 111 | 112 | or 113 | 114 | ```bash 115 | yarn test 116 | yarn run lint 117 | ``` 118 | 119 | ## Based on 120 | 121 | [Smart App Banner](https://github.com/kudago/smart-app-banner) 122 | 123 | ## License 124 | 125 | MIT 126 | 127 | Copyright (c) 2015-2019 patw. 128 | 129 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-var": 0, 4 | "comma-dangle": 0, 5 | "quote-props": 0, 6 | "object-shorthand": 0, 7 | "no-multiple-empty-lines": 0, 8 | "no-trailing-spaces": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, arrow-parens, prefer-template */ 2 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 3 | // injected into the application via DefinePlugin in Webpack configuration. 4 | 5 | const REACT_APP = /^REACT_APP_/i; 6 | 7 | function getClientEnvironment(publicUrl) { 8 | var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development'); 9 | 10 | const DEVELOPMENT = NODE_ENV === JSON.stringify('development'); 11 | const SERVER = false; 12 | const CLIENT = true; 13 | const BUILD_NAME = JSON.stringify(process.env.BUILD_NAME || 'dev'); 14 | 15 | const processEnv = Object.keys(process.env) 16 | .filter(key => REACT_APP.test(key)) 17 | .reduce( 18 | (env, key) => { 19 | env[key] = JSON.stringify(process.env[key]); // eslint-disable-line no-param-reassign 20 | 21 | return env; 22 | }, 23 | { 24 | // Useful for determining whether we’re running in production mode. 25 | // Most importantly, it switches React into the correct mode. 26 | NODE_ENV, 27 | BUILD_NAME, 28 | // Useful for resolving the correct path to static assets in `public`. 29 | // For example, . 30 | // This should only be used as an escape hatch. Normally you would put 31 | // images into the `src` and `import` them in code to get their paths. 32 | PUBLIC_URL: JSON.stringify(publicUrl), 33 | } 34 | ); 35 | 36 | return { 37 | 'process.env': processEnv, 38 | __SERVER__: SERVER, 39 | __CLIENT__: CLIENT, 40 | __DEVELOPMENT__: DEVELOPMENT, 41 | }; 42 | } 43 | 44 | module.exports = getClientEnvironment; 45 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | // This is a custom Jest transformer turning style imports into empty objects. 2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 3 | 4 | module.exports = { 5 | process() { 6 | return ` 7 | const idObj = require('identity-obj-proxy'); 8 | module.exports = idObj; 9 | `; 10 | }, 11 | getCacheKey() { 12 | // eslint-disable-line no-unused-vars 13 | // The output is always the same. 14 | return 'cssTransform'; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // This is a custom Jest transformer turning file imports into filenames. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | // eslint-disable-next-line prefer-template 9 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /config/jest/setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | window.__SERVER__ = false; 4 | window.__DEVELOPMENT__ = false; 5 | 6 | // Define some html to be our basic document 7 | // JSDOM will consume this and act as if we were in a browser 8 | const DEFAULT_HTML = ''; 9 | 10 | // Define some variables to make it look like we're a browser 11 | // First, use JSDOM's fake DOM as the document 12 | const doc = jsdom.jsdom(DEFAULT_HTML); 13 | 14 | global.document = doc; 15 | 16 | // Set up a mock window 17 | global.window = doc.defaultView; 18 | 19 | // Allow for things like window.location 20 | global.navigator = window.navigator; 21 | 22 | const DATE_TO_USE = new Date('2017-05-11'); 23 | const _Date = Date; 24 | 25 | global.Date = jest.fn(() => DATE_TO_USE); 26 | global.Date.UTC = _Date.UTC; 27 | global.Date.parse = _Date.parse; 28 | global.Date.now = _Date.now; 29 | -------------------------------------------------------------------------------- /config/jest/setupTestFramework.js: -------------------------------------------------------------------------------- 1 | /* global jasmine:false */ 2 | import Enzyme from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | if (process.env.CI) { 8 | const jasmineReporters = require('jasmine-reporters'); // eslint-disable-line global-require 9 | const junitReporter = new jasmineReporters.JUnitXmlReporter({ 10 | savePath: 'testresults', 11 | consolidateAll: false, 12 | }); 13 | 14 | jasmine.getEnv().addReporter(junitReporter); 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/transform.js: -------------------------------------------------------------------------------- 1 | const babelJest = require('babel-jest'); 2 | 3 | module.exports = babelJest.createTransformer(); 4 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | const appDirectory = fs.realpathSync(process.cwd()); 7 | 8 | function resolveApp(relativePath) { 9 | return path.resolve(appDirectory, relativePath); 10 | } 11 | 12 | // We support resolving modules according to `NODE_PATH`. 13 | // This lets you use absolute paths in imports inside large monorepos: 14 | // https://github.com/facebookincubator/create-react-app/issues/253. 15 | 16 | // It works similar to `NODE_PATH` in Node itself: 17 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 18 | 19 | // We will export `nodePaths` as an array of absolute paths. 20 | // It will then be used by Webpack configs. 21 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 22 | 23 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 24 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 25 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 26 | 27 | // eslint-disable-next-line vars-on-top 28 | const nodePaths = (process.env.NODE_PATH || '') 29 | .split(process.platform === 'win32' ? ';' : ':') 30 | .filter(Boolean) 31 | .filter(folder => !path.isAbsolute(folder)) 32 | .map(resolveApp); 33 | 34 | // config after eject: we're in ./config/ 35 | module.exports = { 36 | appBuild: resolveApp('example'), 37 | appDist: resolveApp('dist'), 38 | appPublic: resolveApp('public'), 39 | appHtml: resolveApp('src/index.html'), 40 | appExampleJs: resolveApp('src/example.js'), 41 | appPackageJson: resolveApp('package.json'), 42 | appSrc: resolveApp('src'), 43 | yarnLockFile: resolveApp('yarn.lock'), 44 | appNodeModules: resolveApp('node_modules'), 45 | ownNodeModules: resolveApp('node_modules'), 46 | nodePaths: nodePaths, 47 | }; 48 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack development server configuration 3 | * 4 | * This file is set up for serving the webpack-dev-server, which will watch for changes and recompile as required if 5 | * the subfolder /webpack-dev-server/ is visited. Visiting the root will not automatically reload. 6 | */ 7 | 8 | const webpack = require('webpack'); 9 | const paths = require('./paths'); 10 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 11 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 12 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 13 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 14 | const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 15 | const getClientEnvironment = require('./env'); 16 | 17 | // Webpack uses `publicPath` to determine where the app is being served from. 18 | // In development, we always serve from the root. This makes config easier. 19 | const publicPath = '/'; 20 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 21 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 22 | // Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 23 | const publicUrl = ''; 24 | // Get enrivonment variables to inject into our app. 25 | const env = getClientEnvironment(publicUrl); 26 | 27 | module.exports = { 28 | mode: 'development', 29 | devtool: 'cheap-module-source-map', 30 | entry: [ 31 | require.resolve('react-dev-utils/webpackHotDevClient'), 32 | require.resolve('./polyfills'), 33 | paths.appExampleJs, 34 | ], 35 | 36 | output: { 37 | path: paths.appBuild, 38 | pathinfo: true, 39 | filename: 'static/js/bundle.js', 40 | publicPath: publicPath, 41 | }, 42 | 43 | externals: { 44 | react: 'React', 45 | 'react-dom': 'ReactDOM', 46 | }, 47 | 48 | resolve: { 49 | modules: ['src', 'node_modules', ...paths.nodePaths], 50 | alias: { 51 | 'react-smartbanner': './components/SmartBanner.js', 52 | }, 53 | }, 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.(js|jsx)$/, 58 | enforce: 'pre', 59 | include: paths.appSrc, 60 | use: { 61 | loader: 'eslint-loader', 62 | }, 63 | }, 64 | { 65 | exclude: [ 66 | /\.html$/, 67 | /\.(js|jsx)$/, 68 | /\.css$/, 69 | /\.scss$/, 70 | /\.json$/, 71 | /\.png$/, 72 | /\.svg$/, 73 | ], 74 | loader: 'url-loader', 75 | options: { 76 | limit: 10000, 77 | name: 'static/media/[name].[hash:8].[ext]', 78 | }, 79 | }, 80 | { 81 | test: /\.(js|jsx)$/, 82 | include: paths.appSrc, 83 | use: { 84 | loader: 'babel-loader', 85 | }, 86 | }, 87 | { 88 | test: /\.scss$/, 89 | use: [ 90 | 'style-loader?sourceMap', 91 | 'css-loader', 92 | 'sass-loader?outputStyle=expanded', 93 | ], 94 | }, 95 | { 96 | test: /\.css$/, 97 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 98 | }, 99 | ], 100 | }, 101 | 102 | plugins: [ 103 | new webpack.NormalModuleReplacementPlugin( 104 | /^\.\/main\.css$/, 105 | '../dist/main.css' 106 | ), 107 | new CopyWebpackPlugin([{ from: 'src/icon.png', to: './' }]), 108 | new HtmlWebpackPlugin({ 109 | inject: true, 110 | template: paths.appHtml, 111 | }), 112 | new InterpolateHtmlPlugin(HtmlWebpackPlugin, { 113 | PUBLIC_URL: publicUrl, 114 | }), 115 | new webpack.DefinePlugin(env), 116 | new webpack.HotModuleReplacementPlugin(), 117 | new CaseSensitivePathsPlugin(), 118 | new WatchMissingNodeModulesPlugin(paths.appNodeModules), 119 | ], 120 | node: { 121 | fs: 'empty', 122 | net: 'empty', 123 | tls: 'empty', 124 | }, 125 | }; 126 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var, arrow-parens, prefer-template, comma-dangle, object-shorthand, global-require, func-names, no-else-return, vars-on-top */ 2 | 3 | const webpack = require('webpack'); 4 | const paths = require('./paths'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 8 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 9 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 10 | const safeParser = require('postcss-safe-parser'); 11 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 12 | const getClientEnvironment = require('./env'); 13 | 14 | // Webpack uses `publicPath` to determine where the app is being served from. 15 | // In development, we always serve from the root. This makes config easier. 16 | const publicPath = ''; 17 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 18 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 19 | // Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 20 | const publicUrl = ''; 21 | // Get enrivonment variables to inject into our app. 22 | const env = getClientEnvironment(publicUrl); 23 | 24 | // Assert this just to be safe. 25 | // Development builds of React are slow and not intended for production. 26 | if (env['process.env'].NODE_ENV !== '"production"') { 27 | throw new Error('Production builds must have NODE_ENV=production.'); 28 | } 29 | 30 | module.exports = { 31 | mode: 'production', 32 | devtool: false, 33 | entry: { 34 | main: './src/components/SmartBanner.js', 35 | example: [require.resolve('./polyfills'), './src/example.js'], 36 | }, 37 | 38 | output: { 39 | path: paths.appBuild, 40 | pathinfo: true, 41 | filename: '[name].js', 42 | publicPath: publicPath, 43 | library: 'SmartBanner', 44 | libraryTarget: 'umd', 45 | globalObject: 'typeof self !== \'undefined\' ? self : this', 46 | }, 47 | 48 | externals: { 49 | react: { 50 | root: 'React', 51 | commonjs2: 'react', 52 | commonjs: 'react', 53 | amd: 'react', 54 | }, 55 | 'react-dom': { 56 | root: 'ReactDOM', 57 | commonjs2: 'react-dom', 58 | commonjs: 'react-dom', 59 | amd: 'react-dom', 60 | }, 61 | 'prop-types': { 62 | root: 'PropTypes', 63 | commonjs2: 'prop-types', 64 | commonjs: 'prop-types', 65 | amd: 'prop-types', 66 | }, 67 | }, 68 | 69 | resolve: { 70 | modules: ['src', 'node_modules', ...paths.nodePaths], 71 | alias: { 72 | 'react-smartbanner': './components/SmartBanner.js', 73 | }, 74 | }, 75 | module: { 76 | rules: [ 77 | { 78 | test: /\.(js|jsx)$/, 79 | loader: 'eslint-loader', 80 | enforce: 'pre', 81 | include: paths.appSrc, 82 | }, 83 | { 84 | exclude: [ 85 | /\.html$/, 86 | /\.(js|jsx)$/, 87 | /\.css$/, 88 | /\.scss$/, 89 | /\.json$/, 90 | /\.png$/, 91 | /\.svg$/, 92 | ], 93 | loader: 'url-loader', 94 | options: { 95 | limit: 10000, 96 | name: 'media/[name].[hash:8].[ext]', 97 | }, 98 | }, 99 | { 100 | test: /\.(js|jsx)$/, 101 | include: paths.appSrc, 102 | use: { 103 | loader: 'babel-loader', 104 | }, 105 | }, 106 | { 107 | test: /\.scss$/, 108 | use: [ 109 | MiniCssExtractPlugin.loader, 110 | 'css-loader', 111 | 'sass-loader?outputStyle=expanded', 112 | ], 113 | }, 114 | { 115 | test: /\.css$/, 116 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 117 | }, 118 | ], 119 | }, 120 | optimization: { 121 | minimizer: [ 122 | new UglifyJsPlugin({ 123 | cache: true, 124 | parallel: true, 125 | uglifyOptions: { 126 | compress: true, 127 | ecma: 6, 128 | mangle: true, 129 | output: { 130 | comments: false, 131 | beautify: false, 132 | }, 133 | }, 134 | sourceMap: false, 135 | }), 136 | new OptimizeCssAssetsPlugin({ 137 | cssProcessorOptions: { 138 | parser: safeParser, 139 | discardComments: { 140 | removeAll: true, 141 | }, 142 | }, 143 | }), 144 | ], 145 | }, 146 | 147 | plugins: [ 148 | new webpack.LoaderOptionsPlugin({ 149 | minimize: true, 150 | debug: false, 151 | }), 152 | new HtmlWebpackPlugin({ 153 | inject: true, 154 | template: paths.appHtml, 155 | chunks: ['example'], 156 | minify: { 157 | removeComments: true, 158 | collapseWhitespace: true, 159 | removeRedundantAttributes: true, 160 | useShortDoctype: true, 161 | removeEmptyAttributes: true, 162 | removeStyleLinkTypeAttributes: true, 163 | keepClosingSlash: true, 164 | minifyJS: true, 165 | minifyCSS: true, 166 | minifyURLs: true, 167 | }, 168 | }), 169 | // Generates an `index.html` file with the

If you can see this, something is broken (or JS is not enabled)!!.

Installation

npm install --save react-smartbanner

or...

yarn add react-smartbanner

Syntax

General

Remember to add following meta tags in your HTML page: (Use Facebook app as example)

<head>
 4 |   <meta name="apple-itunes-app" content="app-id=284882215">
 5 |   <meta name="google-play-app" content="app-id=com.facebook.katana">
 6 |   <meta name="msApplication-ID" content="82a23635-5bd9-df11-a844-00237de2db9e">
 7 |   <meta name="msApplication-PackageFamilyName" content="facebook_9wzdncrfhv5g">
 8 |   <meta name="kindle-fire-app" content="app-id=B0094BB4TW">
 9 | 
10 |   <link rel="apple-touch-icon" href="icon.png">
11 |   <link rel="android-touch-icon" href="icon.png">
12 |   <link rel="windows-touch-icon" href="icon.png">
13 | </head>
14 | 

And React-SmartBanner component usage:


15 | import React from 'react';
16 | import ReactDOM from 'react-dom';
17 | import SmartBanner from 'react-smartbanner';
18 | import './node_modules/react-smartbanner/dist/main.css';
19 | 
20 | ReactDOM.render(<SmartBanner title={'Facebook'} />, document.getElementById('content'));
21 | 

Props

keydefaultdescription
daysHidden15Days to hide banner after close button is clicked.
daysReminder90Days to hide banner after "VIEW" button is clicked.
appStoreLanguage(user's browser language)Language code for the App Store.
title''App title.
author''App author.
button'View'Display on install button. (node)
storeText{ ios: 'On the App Store', android: 'In Google Play', windows: 'In Windows store', kindle: 'In Amazon Appstore' }Store text (object).
price{ ios: 'FREE', android: 'FREE', windows: 'FREE', kindle: 'FREE' }Price text (object).
position'top' / 'bottom'Display position on screen. Bottom is fixed, top scrolls out.
force''Force to display in specific device.
(android, ios, windows, kindle)
url{ ios: 'http://www.domain.com', android: 'http://www.domain2.com', windows: 'http://www.domain3.com', kindle: 'http://www.domain4.com' }Custom URL for each device
ignoreIosVersionfalseBoolean to ignore the iOS version, so that the banner is also displayed on devices that support the native banner.
appMeta{ ios: 'apple-itunes-app', android: 'google-play-app', windows: 'msApplication-ID', kindle: 'kindle-fire-app' }The custom meta tag name (object).
It provide an option to enforce using custom meta tag to show js react-smartbanner for newer iOS versions.
onCloseNo default valueOptional callback when user clicks on close button.
onInstallNo default valueOptional callback when user clicks on install button.

Based on


Lincense

MIT License

-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-smartbanner", 3 | "version": "5.1.4", 4 | "description": "Smart app banner react version.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/patw0929/react-smartbanner" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/patw0929/react-smartbanner/issues" 11 | }, 12 | "main": "dist/main.js", 13 | "peerDependencies": { 14 | "react": "16.x", 15 | "react-dom": "16.x" 16 | }, 17 | "dependencies": { 18 | "cookie-cutter": "^0.1.1", 19 | "cookies-js": "^1.2.1", 20 | "prop-types": "^15.5.8", 21 | "ua-parser-js": "^0.7.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.2.0", 25 | "@babel/plugin-proposal-class-properties": "^7.2.1", 26 | "@babel/preset-env": "^7.2.0", 27 | "@babel/preset-react": "^7.0.0", 28 | "babel-core": "^7.0.0-bridge.0", 29 | "babel-eslint": "^10.0.1", 30 | "babel-jest": "^23.6.0", 31 | "babel-loader": "^8.0.4", 32 | "case-sensitive-paths-webpack-plugin": "^2.1.2", 33 | "chalk": "1.1.3", 34 | "copy-webpack-plugin": "^4.6.0", 35 | "coveralls": "^2.13.1", 36 | "css-loader": "^1.0.1", 37 | "detect-port": "^1.0.7", 38 | "dotenv": "^4.0.0", 39 | "enzyme": "^3.7.0", 40 | "enzyme-adapter-react-16": "^1.7.0", 41 | "eslint": "^5.3.0", 42 | "eslint-config-airbnb": "~17.1.0", 43 | "eslint-config-airbnb-base": "~13.1.0", 44 | "eslint-config-prettier": "^3.0.1", 45 | "eslint-loader": "^2.1.1", 46 | "eslint-plugin-import": "^2.14.0", 47 | "eslint-plugin-jsx-a11y": "^6.1.1", 48 | "eslint-plugin-react": "^7.11.0", 49 | "eslint-plugin-security": "^1.3.0", 50 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 51 | "file-loader": "^2.0.0", 52 | "filesize": "^3.3.0", 53 | "fs-extra": "^2.1.2", 54 | "gh-pages": "^0.12.0", 55 | "gzip-size": "3.0.0", 56 | "html-webpack-plugin": "^4.0.0-beta.4", 57 | "identity-obj-proxy": "^3.0.0", 58 | "jasmine-reporters": "^2.2.0", 59 | "jest": "^23.6.0", 60 | "jsdom": "^9.2.1", 61 | "lint-staged": "^3.2.6", 62 | "mini-css-extract-plugin": "^0.4.5", 63 | "node-sass": "^4.5.3", 64 | "optimize-css-assets-webpack-plugin": "^5.0.1", 65 | "postcss-safe-parser": "^4.0.1", 66 | "pre-commit": "^1.2.2", 67 | "prettier": "^1.14.2", 68 | "react": "^16.9.0", 69 | "react-dev-utils": "^6.1.1", 70 | "react-dom": "^16.9.0", 71 | "react-hot-loader": "^1.3.0", 72 | "recursive-readdir": "2.1.0", 73 | "rimraf": "2.5.4", 74 | "sass-lint": "^1.10.2", 75 | "sass-loader": "^7.1.0", 76 | "serve-static": "^1.11.1", 77 | "style-loader": "^0.23.1", 78 | "uglifyjs-webpack-plugin": "^2.0.1", 79 | "url-loader": "~0.5.7", 80 | "webpack": "^4.27.0", 81 | "webpack-dev-server": "^3.1.10" 82 | }, 83 | "scripts": { 84 | "build": "node scripts/build.js", 85 | "start": "node scripts/start.js", 86 | "deploy": "gh-pages -d example", 87 | "lint-staged": "lint-staged", 88 | "lint-pass": "echo '\\033[4;32m♡' No any errors! Go go go! ♡' \\033[0m'", 89 | "lint": "yarn run eslint && yarn run sass-lint", 90 | "eslint": "eslint src", 91 | "sass-lint": "sass-lint -v", 92 | "test": "TZ=Asia/Taipei node scripts/test.js --env=jsdom", 93 | "coverage": "yarn run test -- --coverage", 94 | "coveralls": "NODE_ENV=development cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 95 | "prettier:all": "prettier --write 'src/example.js' 'src/components/**/*.js' 'config/**/*.js'" 96 | }, 97 | "lint-staged": { 98 | "*.js": [ 99 | "yarn prettier --write", 100 | "git add", 101 | "jest --findRelatedTests", 102 | "eslint --max-warnings 0" 103 | ], 104 | "*.scss": [ 105 | "sass-lint --max-warnings 0 -v" 106 | ] 107 | }, 108 | "pre-commit": [ 109 | "lint-staged", 110 | "lint-pass" 111 | ], 112 | "prettier": { 113 | "printWidth": 80, 114 | "singleQuote": true, 115 | "trailingComma": "es5" 116 | }, 117 | "jest": { 118 | "collectCoverageFrom": [ 119 | "src/**/*.js", 120 | "!**/__mocks__/**", 121 | "!**/__tests__/**", 122 | "!src/example.js" 123 | ], 124 | "setupFiles": [ 125 | "/config/polyfills.js", 126 | "/config/jest/setup.js" 127 | ], 128 | "setupTestFrameworkScriptFile": "/config/jest/setupTestFramework.js", 129 | "testPathIgnorePatterns": [ 130 | "[/\\\\](build|docs|node_modules)[/\\\\]" 131 | ], 132 | "testEnvironment": "jsdom", 133 | "testURL": "http://localhost", 134 | "transform": { 135 | "^.+\\.(js|jsx)$": "/config/jest/transform.js", 136 | "^.+\\.(scss|css)$": "/config/jest/cssTransform.js", 137 | "^(?!.*\\.(js|jsx|css|scss|json)$)": "/config/jest/fileTransform.js" 138 | }, 139 | "transformIgnorePatterns": [ 140 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 141 | ], 142 | "testRegex": "/__tests__/.*\\.(test|spec)\\.js$" 143 | }, 144 | "keywords": [ 145 | "react", 146 | "component", 147 | "smartbanner", 148 | "ios", 149 | "iphone", 150 | "android", 151 | "windowsphone", 152 | "smart", 153 | "banner" 154 | ], 155 | "author": "patw (http://patw.me/)", 156 | "engines": { 157 | "node": ">=6.2.2" 158 | }, 159 | "license": "MIT" 160 | } 161 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies,import/no-dynamic-require,global-require */ 2 | // Do this as the first thing so that any code reading it knows the right env. 3 | process.env.NODE_ENV = 'production'; 4 | 5 | // Load environment variables from .env file. Surpress warnings using silent 6 | // if this file is missing. dotenv will never modify any environment variables 7 | // that have already been set. 8 | // https://github.com/motdotla/dotenv 9 | require('dotenv').config({ silent: true }); 10 | 11 | const chalk = require('chalk'); 12 | const fs = require('fs-extra'); 13 | const path = require('path'); 14 | const gzipSize = require('gzip-size').sync; 15 | const rimrafSync = require('rimraf').sync; 16 | const webpack = require('webpack'); 17 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 18 | const recursive = require('recursive-readdir'); 19 | const config = require('../config/webpack.config.prod'); 20 | const paths = require('../config/paths'); 21 | 22 | // Warn and crash if required files are missing 23 | if (!checkRequiredFiles([paths.appHtml, paths.appExampleJs])) { 24 | process.exit(1); 25 | } 26 | 27 | // Input: /User/dan/app/build/static/js/main.82be8.js 28 | // Output: /static/js/main.js 29 | function removeFileNameHash(fileName) { 30 | return fileName 31 | .replace(paths.appBuild, '') 32 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 33 | } 34 | 35 | // Input: 1024, 2048 36 | // Output: "(+1 KB)" 37 | // function getDifferenceLabel(currentSize, previousSize) { 38 | // const FIFTY_KILOBYTES = 1024 * 50; 39 | // const difference = currentSize - previousSize; 40 | // const fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 41 | 42 | // if (difference >= FIFTY_KILOBYTES) { 43 | // return chalk.red(`+${fileSize}`); 44 | // } 45 | 46 | // if (difference < FIFTY_KILOBYTES && difference > 0) { 47 | // return chalk.yellow(`+${fileSize}`); 48 | // } 49 | 50 | // if (difference < 0) { 51 | // return chalk.green(fileSize); 52 | // } 53 | 54 | // return ''; 55 | // } 56 | 57 | // First, read the current file sizes in build directory. 58 | // This lets us display how much they changed later. 59 | recursive(paths.appBuild, (err, fileNames) => { 60 | const previousSizeMap = (fileNames || []) 61 | .filter((fileName) => /\.(js|css)$/.test(fileName)) 62 | .reduce((memo, fileName) => { 63 | const contents = fs.readFileSync(fileName); 64 | const key = removeFileNameHash(fileName); 65 | 66 | memo[key] = gzipSize(contents); 67 | 68 | return memo; 69 | }, {}); 70 | 71 | // Remove all content but keep the directory so that 72 | // if you're in it, you don't end up in Trash 73 | rimrafSync(`${paths.appBuild}/*`); 74 | rimrafSync(`${paths.appDist}/*`); 75 | 76 | // Start the webpack build 77 | // eslint-disable-next-line no-use-before-define 78 | build(previousSizeMap); 79 | }); 80 | 81 | // Print a detailed summary of build files. 82 | // function printFileSizes(stats, previousSizeMap) { 83 | // const assets = stats.toJson().assets 84 | // .filter((asset) => /\.(js|css)$/.test(asset.name)) 85 | // .map((asset) => { 86 | // const fileContents = fs.readFileSync(`${paths.appSrc}/${asset.name}`); 87 | // const size = gzipSize(fileContents); 88 | // const previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 89 | // const difference = getDifferenceLabel(size, previousSize); 90 | 91 | // return { 92 | // folder: path.join('build', path.dirname(asset.name)), 93 | // name: path.basename(asset.name), 94 | // sizeLabel: filesize(size) + (difference ? ` (${difference})` : ''), 95 | // size, 96 | // }; 97 | // }); 98 | 99 | // assets.sort((a, b) => b.size - a.size); 100 | // const longestSizeLabelLength = Math.max.apply(null, 101 | // assets.map((a) => stripAnsi(a.sizeLabel).length) 102 | // ); 103 | 104 | // assets.forEach((asset) => { 105 | // let sizeLabel = asset.sizeLabel; 106 | // const sizeLength = stripAnsi(sizeLabel).length; 107 | 108 | // if (sizeLength < longestSizeLabelLength) { 109 | // const rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 110 | 111 | // sizeLabel += rightPadding; 112 | // } 113 | // console.log( 114 | // ` ${sizeLabel} ${chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)}` 115 | // ); 116 | // }); 117 | // } 118 | 119 | // Create the production build and print the deployment instructions. 120 | function build(/* previousSizeMap */) { 121 | console.log('Creating an optimized production build...'); 122 | webpack(config).run((err) => { 123 | if (err) { 124 | console.error('Failed to create a production build. Reason:'); 125 | console.error(err.message || err); 126 | process.exit(1); 127 | } 128 | 129 | console.log(chalk.green('Compiled successfully.')); 130 | console.log(); 131 | 132 | // console.log('File sizes after gzip:'); 133 | // console.log(); 134 | // printFileSizes(stats, previousSizeMap); 135 | // console.log(); 136 | 137 | // Copy static files to dist folder 138 | // eslint-disable-next-line no-use-before-define 139 | copyToDistFolder(); 140 | 141 | const openCommand = process.platform === 'win32' ? 'start' : 'open'; 142 | const homepagePath = require(paths.appPackageJson).homepage; 143 | const publicPath = config.output.publicPath; 144 | 145 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 146 | // "homepage": "http://user.github.io/project" 147 | console.log(`The project was built assuming it is hosted at ${chalk.green(publicPath)}.`); 148 | console.log(`You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.`); 149 | console.log(); 150 | console.log(`The ${chalk.cyan('dist')} folder is ready to be deployed.`); 151 | console.log(`To publish it at ${chalk.green(homepagePath)}, run:`); 152 | console.log(); 153 | console.log(` ${chalk.cyan('git')} commit -am ${chalk.yellow('"Save local changes"')}`); 154 | console.log(` ${chalk.cyan('git')} checkout -B gh-pages`); 155 | console.log(` ${chalk.cyan('git')} add -f dist`); 156 | console.log(` ${chalk.cyan('git')} commit -am ${chalk.yellow('"Rebuild website"')}`); 157 | console.log(` ${chalk.cyan('git')} filter-branch -f --prune-empty --subdirectory-filter dist`); 158 | console.log(` ${chalk.cyan('git')} push -f origin gh-pages`); 159 | console.log(` ${chalk.cyan('git')} checkout -`); 160 | console.log(); 161 | } else if (publicPath !== '/') { 162 | // "homepage": "http://mywebsite.com/project" 163 | console.log(`The project was built assuming it is hosted at ${chalk.green(publicPath)}.`); 164 | console.log(`You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.`); 165 | console.log(); 166 | console.log(`The ${chalk.cyan('dist')} folder is ready to be deployed.`); 167 | console.log(); 168 | } else { 169 | // no homepage or "homepage": "http://mywebsite.com" 170 | console.log('The project was built assuming it is hosted at the server root.'); 171 | if (homepagePath) { 172 | // "homepage": "http://mywebsite.com" 173 | console.log(`You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.`); 174 | console.log(); 175 | } else { 176 | // no homepage 177 | console.log(`To override this, specify the ${chalk.green('homepage')} in your ${chalk.cyan('package.json')}.`); 178 | console.log('For example, add this to build it for GitHub Pages:'); 179 | console.log(); 180 | console.log(` ${chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')}`); 181 | console.log(); 182 | } 183 | console.log(`The ${chalk.cyan('dist')} folder is ready to be deployed.`); 184 | console.log('You may also serve it locally with a static server:'); 185 | console.log(); 186 | console.log(` ${chalk.cyan('npm')} install -g pushstate-server`); 187 | console.log(` ${chalk.cyan('pushstate-server')} build`); 188 | console.log(` ${chalk.cyan(openCommand)} http://localhost:9000`); 189 | console.log(); 190 | } 191 | }); 192 | } 193 | 194 | function copyToDistFolder() { 195 | fs.copySync(`${paths.appBuild}/`, paths.appDist, { 196 | filter: (file) => { 197 | const target = path.basename(file); 198 | 199 | if (target !== 'index.html' && target !== 'example.js' && target !== 'icon.png') { 200 | return true; 201 | } 202 | 203 | return false; 204 | }, 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies,import/no-dynamic-require,global-require */ 2 | process.env.NODE_ENV = 'development'; 3 | 4 | // Load environment variables from .env file. Surpress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({ silent: true }); 9 | 10 | const chalk = require('chalk'); 11 | const webpack = require('webpack'); 12 | const WebpackDevServer = require('webpack-dev-server'); 13 | const historyApiFallback = require('connect-history-api-fallback'); 14 | const httpProxyMiddleware = require('http-proxy-middleware'); 15 | const detect = require('detect-port'); 16 | const clearConsole = require('react-dev-utils/clearConsole'); 17 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 18 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 19 | const openBrowser = require('react-dev-utils/openBrowser'); 20 | // const prompt = require('react-dev-utils/prompt'); 21 | const config = require('../config/webpack.config.dev'); 22 | const paths = require('../config/paths'); 23 | 24 | // Warn and crash if required files are missing 25 | if (!checkRequiredFiles([paths.appHtml, paths.appExampleJs])) { 26 | process.exit(1); 27 | } 28 | 29 | // Tools like Cloud9 rely on this. 30 | const DEFAULT_PORT = process.env.PORT || 3000; 31 | let compiler; 32 | let handleCompile; 33 | 34 | // You can safely remove this after ejecting. 35 | // We only use this block for testing of Create React App itself: 36 | const isSmokeTest = process.argv.some((arg) => arg.indexOf('--smoke-test') > -1); 37 | 38 | if (isSmokeTest) { 39 | handleCompile = (err, stats) => { 40 | if (err || stats.hasErrors() || stats.hasWarnings()) { 41 | process.exit(1); 42 | } else { 43 | process.exit(0); 44 | } 45 | }; 46 | } 47 | 48 | function setupCompiler(host, port, protocol) { 49 | // "Compiler" is a low-level interface to Webpack. 50 | // It lets us listen to some events and provide our own custom messages. 51 | compiler = webpack(config, handleCompile); 52 | 53 | // "invalid" event fires when you have changed a file, and Webpack is 54 | // recompiling a bundle. WebpackDevServer takes care to pause serving the 55 | // bundle, so if you refresh, it'll wait instead of serving the old one. 56 | // "invalid" is short for "bundle invalidated", it doesn't imply any errors. 57 | compiler.plugin('invalid', () => { 58 | clearConsole(); 59 | console.log('Compiling...'); 60 | }); 61 | 62 | // "done" event fires when Webpack has finished recompiling the bundle. 63 | // Whether or not you have warnings or errors, you will get this event. 64 | compiler.plugin('done', (stats) => { 65 | clearConsole(); 66 | 67 | // We have switched off the default Webpack output in WebpackDevServer 68 | // options so we are going to "massage" the warnings and errors and present 69 | // them in a readable focused way. 70 | const messages = formatWebpackMessages(stats.toJson({}, true)); 71 | 72 | if (!messages.errors.length && !messages.warnings.length) { 73 | console.log(chalk.green('Compiled successfully!')); 74 | console.log(); 75 | console.log('The app is running at:'); 76 | console.log(); 77 | console.log(` ${chalk.cyan(`${protocol}://${host}:${port}/`)}`); 78 | console.log(); 79 | console.log('Note that the development build is not optimized.'); 80 | console.log(`To create a production build, use ${chalk.cyan('npm run build')}.`); 81 | console.log(); 82 | } 83 | 84 | // If errors exist, only show errors. 85 | if (messages.errors.length) { 86 | console.log(chalk.red('Failed to compile.')); 87 | console.log(); 88 | messages.errors.forEach((message) => { 89 | console.log(message); 90 | console.log(); 91 | }); 92 | 93 | return; 94 | } 95 | 96 | // Show warnings if no errors were found. 97 | if (messages.warnings.length) { 98 | console.log(chalk.yellow('Compiled with warnings.')); 99 | console.log(); 100 | messages.warnings.forEach((message) => { 101 | console.log(message); 102 | console.log(); 103 | }); 104 | // Teach some ESLint tricks. 105 | console.log('You may use special comments to disable some warnings.'); 106 | console.log(`Use ${chalk.yellow('// eslint-disable-next-line')} to ignore the next line.`); 107 | console.log(`Use ${chalk.yellow('/* eslint-disable */')} to ignore all warnings in a file.`); 108 | } 109 | }); 110 | } 111 | 112 | // We need to provide a custom onError function for httpProxyMiddleware. 113 | // It allows us to log custom error messages on the console. 114 | function onProxyError(proxy) { 115 | return (err, req, res) => { 116 | const host = req.headers && req.headers.host; 117 | 118 | console.log( 119 | `${chalk.red('Proxy error:')} Could not proxy request ${chalk.cyan(req.url)} 120 | from ${chalk.cyan(host)} to ${chalk.cyan(proxy)}.` 121 | ); 122 | console.log( 123 | `See https://nodejs.org/api/errors.html#errors_common_system_errors for more information ( 124 | ${chalk.cyan(err.code)}).` 125 | ); 126 | console.log(); 127 | 128 | // And immediately send the proper error response to the client. 129 | // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side. 130 | if (res.writeHead && !res.headersSent) { 131 | res.writeHead(500); 132 | } 133 | res.end(`Proxy error: Could not proxy request ${req.url} from 134 | ${host} to ${proxy} (${err.code}).` 135 | ); 136 | }; 137 | } 138 | 139 | function addMiddleware(devServer) { 140 | // `proxy` lets you to specify a fallback server during development. 141 | // Every unrecognized request will be forwarded to it. 142 | const proxy = require(paths.appPackageJson).proxy; 143 | 144 | devServer.use(historyApiFallback({ 145 | // Paths with dots should still use the history fallback. 146 | // See https://github.com/facebookincubator/create-react-app/issues/387. 147 | disableDotRule: true, 148 | // For single page apps, we generally want to fallback to /index.html. 149 | // However we also want to respect `proxy` for API calls. 150 | // So if `proxy` is specified, we need to decide which fallback to use. 151 | // We use a heuristic: if request `accept`s text/html, we pick /index.html. 152 | // Modern browsers include text/html into `accept` header when navigating. 153 | // However API calls like `fetch()` won’t generally accept text/html. 154 | // If this heuristic doesn’t work well for you, don’t use `proxy`. 155 | htmlAcceptHeaders: proxy ? 156 | ['text/html'] : 157 | ['text/html', '*/*'], 158 | })); 159 | if (proxy) { 160 | if (typeof proxy !== 'string') { 161 | console.log(chalk.red('When specified, "proxy" in package.json must be a string.')); 162 | console.log(chalk.red(`Instead, the type of "proxy" was "${typeof proxy}".`)); 163 | console.log(chalk.red('Either remove "proxy" from package.json, or make it a string.')); 164 | process.exit(1); 165 | } 166 | 167 | // Otherwise, if proxy is specified, we will let it handle any request. 168 | // There are a few exceptions which we won't send to the proxy: 169 | // - /index.html (served as HTML5 history API fallback) 170 | // - /*.hot-update.json (WebpackDevServer uses this too for hot reloading) 171 | // - /sockjs-node/* (WebpackDevServer uses this for hot reloading) 172 | // Tip: use https://jex.im/regulex/ to visualize the regex 173 | const mayProxy = /^(?!\/(index\.html$|.*\.hot-update\.json$|sockjs-node\/)).*$/; 174 | 175 | devServer.use(mayProxy, 176 | // Pass the scope regex both to Express and to the middleware for proxying 177 | // of both HTTP and WebSockets to work without false positives. 178 | httpProxyMiddleware((pathname) => mayProxy.test(pathname), { 179 | target: proxy, 180 | logLevel: 'silent', 181 | onError: onProxyError(proxy), 182 | secure: false, 183 | changeOrigin: true, 184 | }) 185 | ); 186 | } 187 | // Finally, by now we have certainly resolved the URL. 188 | // It may be /index.html, so let the dev server try serving it again. 189 | devServer.use(devServer.middleware); 190 | } 191 | 192 | function runDevServer(host, port, protocol) { 193 | const devServer = new WebpackDevServer(compiler, { 194 | // Silence WebpackDevServer's own logs since they're generally not useful. 195 | // It will still show compile warnings and errors with this setting. 196 | clientLogLevel: 'none', 197 | // By default WebpackDevServer serves physical files from current directory 198 | // in addition to all the virtual build products that it serves from memory. 199 | // This is confusing because those files won’t automatically be available in 200 | // production build folder unless we copy them. However, copying the whole 201 | // project directory is dangerous because we may expose sensitive files. 202 | // Instead, we establish a convention that only files in `public` directory 203 | // get served. Our build script will copy `public` into the `build` folder. 204 | // In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%: 205 | // 206 | // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. 207 | // Note that we only recommend to use `public` folder as an escape hatch 208 | // for files like `favicon.ico`, `manifest.json`, and libraries that are 209 | // for some reason broken when imported through Webpack. If you just want to 210 | // use an image, put it in `src` and `import` it from JavaScript instead. 211 | contentBase: paths.appPublic, 212 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint 213 | // for the WebpackDevServer client so it can learn when the files were 214 | // updated. The WebpackDevServer client is included as an entry point 215 | // in the Webpack development configuration. Note that only changes 216 | // to CSS are currently hot reloaded. JS changes will refresh the browser. 217 | hot: true, 218 | // It is important to tell WebpackDevServer to use the same "root" path 219 | // as we specified in the config. In development, we always serve from /. 220 | publicPath: config.output.publicPath, 221 | // WebpackDevServer is noisy by default so we emit custom message instead 222 | // by listening to the compiler events with `compiler.plugin` calls above. 223 | quiet: true, 224 | // Reportedly, this avoids CPU overload on some systems. 225 | // https://github.com/facebookincubator/create-react-app/issues/293 226 | watchOptions: { 227 | ignored: /node_modules/, 228 | }, 229 | // Enable HTTPS if the HTTPS environment variable is set to 'true' 230 | https: protocol === 'https', 231 | host, 232 | }); 233 | 234 | // Our custom middleware proxies requests to /index.html or a remote API. 235 | addMiddleware(devServer); 236 | 237 | // Launch WebpackDevServer. 238 | devServer.listen(port, (err) => { 239 | if (err) { 240 | return console.log(err); 241 | } 242 | 243 | clearConsole(); 244 | console.log(chalk.cyan('Starting the development server...')); 245 | console.log(); 246 | openBrowser(`${protocol}://${host}:${port}/`); 247 | 248 | return null; 249 | }); 250 | } 251 | 252 | function run(port) { 253 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 254 | const host = process.env.HOST || 'localhost'; 255 | 256 | setupCompiler(host, port, protocol); 257 | runDevServer(host, port, protocol); 258 | } 259 | 260 | // We attempt to use the default port but if it is busy, we offer the user to 261 | // run on a different port. `detect()` Promise resolves to the next free port. 262 | detect(DEFAULT_PORT).then((port) => { 263 | if (port === DEFAULT_PORT) { 264 | run(port); 265 | 266 | return; 267 | } 268 | 269 | clearConsole(); 270 | }); 271 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Surpress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({ silent: true }); 9 | 10 | const jest = require('jest'); 11 | 12 | const argv = process.argv.slice(2); 13 | 14 | // Watch unless on CI 15 | if (!process.env.CI) { 16 | argv.push('--watch'); 17 | } 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /src/components/SmartBanner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../styles/style.scss'; 4 | 5 | const isClient = typeof window !== 'undefined'; 6 | let ua; 7 | let cookie; 8 | 9 | const expiredDateInUTC = additionalDays => { 10 | const expiredDate = new Date(); 11 | 12 | expiredDate.setDate(expiredDate.getDate() + additionalDays); 13 | 14 | return expiredDate.toUTCString(); 15 | }; 16 | 17 | class SmartBanner extends Component { 18 | static propTypes = { 19 | daysHidden: PropTypes.number, 20 | daysReminder: PropTypes.number, 21 | appStoreLanguage: PropTypes.string, 22 | button: PropTypes.node, 23 | storeText: PropTypes.objectOf(PropTypes.string), 24 | price: PropTypes.objectOf(PropTypes.string), 25 | force: PropTypes.string, 26 | title: PropTypes.string, 27 | author: PropTypes.string, 28 | position: PropTypes.string, 29 | url: PropTypes.objectOf(PropTypes.string), 30 | ignoreIosVersion: PropTypes.bool, 31 | appMeta: PropTypes.shape({ 32 | android: PropTypes.string, 33 | ios: PropTypes.string, 34 | windows: PropTypes.string, 35 | kindle: PropTypes.string, 36 | }), 37 | onClose: PropTypes.func, 38 | onInstall: PropTypes.func, 39 | }; 40 | 41 | static defaultProps = { 42 | daysHidden: 15, 43 | daysReminder: 90, 44 | appStoreLanguage: isClient 45 | ? (window.navigator.language || window.navigator.userLanguage).slice( 46 | -2 47 | ) || 'us' 48 | : 'us', 49 | button: 'View', 50 | storeText: { 51 | ios: 'On the App Store', 52 | android: 'In Google Play', 53 | windows: 'In Windows Store', 54 | kindle: 'In the Amazon Appstore', 55 | }, 56 | price: { 57 | ios: 'Free', 58 | android: 'Free', 59 | windows: 'Free', 60 | kindle: 'Free', 61 | }, 62 | force: '', 63 | title: '', 64 | author: '', 65 | position: 'top', 66 | url: { 67 | ios: '', 68 | android: '', 69 | windows: '', 70 | kindle: '', 71 | }, 72 | appMeta: { 73 | ios: 'apple-itunes-app', 74 | android: 'google-play-app', 75 | windows: 'msApplication-ID', 76 | kindle: 'kindle-fire-app', 77 | }, 78 | }; 79 | 80 | constructor(props) { 81 | super(props); 82 | 83 | if (!__SERVER__) { 84 | ua = require('ua-parser-js'); // eslint-disable-line global-require 85 | cookie = require('cookie-cutter'); // eslint-disable-line global-require 86 | } 87 | 88 | this.state = { 89 | type: '', 90 | appId: '', 91 | settings: {}, 92 | }; 93 | } 94 | 95 | UNSAFE_componentWillMount() { 96 | this.setType(this.props.force); 97 | } 98 | 99 | UNSAFE_componentWillReceiveProps(nextProps) { 100 | if (nextProps.force !== this.props.force) { 101 | this.setType(nextProps.force); 102 | } 103 | if (nextProps.position === 'top') { 104 | window.document 105 | .querySelector('html') 106 | .classList.add('smartbanner-margin-top'); 107 | window.document 108 | .querySelector('html') 109 | .classList.remove('smartbanner-margin-bottom'); 110 | } else if (nextProps.position === 'bottom') { 111 | window.document 112 | .querySelector('html') 113 | .classList.add('smartbanner-margin-bottom'); 114 | window.document 115 | .querySelector('html') 116 | .classList.remove('smartbanner-margin-top'); 117 | } 118 | } 119 | 120 | componentWillUnmount() { 121 | const documentRoot = window.document.querySelector('html'); 122 | 123 | documentRoot.classList.remove('smartbanner-show'); 124 | documentRoot.classList.remove('smartbanner-margin-top'); 125 | documentRoot.classList.remove('smartbanner-margin-bottom'); 126 | } 127 | 128 | setType(deviceType) { 129 | let type; 130 | 131 | if (isClient) { 132 | const agent = ua(window.navigator.userAgent); 133 | 134 | if (deviceType) { 135 | // force set case 136 | type = deviceType; 137 | } else if ( 138 | agent.os.name === 'Windows Phone' || 139 | agent.os.name === 'Windows Mobile' 140 | ) { 141 | type = 'windows'; 142 | // iOS >= 6 has native support for Smart Banner 143 | } else if ( 144 | agent.os.name === 'iOS' && 145 | (this.props.ignoreIosVersion || 146 | parseInt(agent.os.version, 10) < 6 || 147 | agent.browser.name !== 'Mobile Safari') 148 | ) { 149 | type = 'ios'; 150 | } else if ( 151 | agent.device.vender === 'Amazon' || 152 | agent.browser.name === 'Silk' 153 | ) { 154 | type = 'kindle'; 155 | } else if (agent.os.name === 'Android') { 156 | type = 'android'; 157 | } 158 | } 159 | 160 | this.setState( 161 | { 162 | type, 163 | }, 164 | () => { 165 | if (type) { 166 | this.setSettingsByType(); 167 | } 168 | } 169 | ); 170 | } 171 | 172 | setSettingsByType() { 173 | const mixins = { 174 | ios: { 175 | appMeta: () => this.props.appMeta.ios, 176 | iconRels: ['apple-touch-icon-precomposed', 'apple-touch-icon'], 177 | getStoreLink: () => 178 | `https://itunes.apple.com/${this.props.appStoreLanguage}/app/id`, 179 | }, 180 | android: { 181 | appMeta: () => this.props.appMeta.android, 182 | iconRels: [ 183 | 'android-touch-icon', 184 | 'apple-touch-icon-precomposed', 185 | 'apple-touch-icon', 186 | ], 187 | getStoreLink: () => 'http://play.google.com/store/apps/details?id=', 188 | }, 189 | windows: { 190 | appMeta: () => this.props.appMeta.windows, 191 | iconRels: [ 192 | 'windows-touch-icon', 193 | 'apple-touch-icon-precomposed', 194 | 'apple-touch-icon', 195 | ], 196 | getStoreLink: () => 'http://www.windowsphone.com/s?appid=', 197 | }, 198 | kindle: { 199 | appMeta: () => this.props.appMeta.kindle, 200 | iconRels: [ 201 | 'windows-touch-icon', 202 | 'apple-touch-icon-precomposed', 203 | 'apple-touch-icon', 204 | ], 205 | getStoreLink: () => 'amzn://apps/android?asin=', 206 | }, 207 | }; 208 | 209 | this.setState(prevState => ({ 210 | settings: mixins[prevState.type], 211 | }), () => { 212 | if (this.state.type) { 213 | this.parseAppId(); 214 | } 215 | }); 216 | } 217 | 218 | hide = () => { 219 | if (isClient) { 220 | window.document 221 | .querySelector('html') 222 | .classList.remove('smartbanner-show'); 223 | } 224 | }; 225 | 226 | show = () => { 227 | if (isClient) { 228 | window.document.querySelector('html').classList.add('smartbanner-show'); 229 | } 230 | }; 231 | 232 | close = () => { 233 | this.hide(); 234 | cookie.set('smartbanner-closed', 'true', { 235 | path: '/', 236 | expires: expiredDateInUTC(this.props.daysHidden), 237 | }); 238 | 239 | if (this.props.onClose && typeof this.props.onClose === 'function') { 240 | this.props.onClose(); 241 | } 242 | }; 243 | 244 | install = () => { 245 | this.hide(); 246 | cookie.set('smartbanner-installed', 'true', { 247 | path: '/', 248 | expires: expiredDateInUTC(this.props.daysReminder), 249 | }); 250 | 251 | if (this.props.onInstall && typeof this.props.onInstall === 'function') { 252 | this.props.onInstall(); 253 | } 254 | }; 255 | 256 | parseAppId() { 257 | if (!isClient) { 258 | return ''; 259 | } 260 | 261 | const meta = window.document.querySelector( 262 | `meta[name="${this.state.settings.appMeta()}"]` 263 | ); 264 | 265 | if (!meta) { 266 | return ''; 267 | } 268 | 269 | let appId = ''; 270 | 271 | if (this.state.type === 'windows') { 272 | appId = meta.getAttribute('content'); 273 | } else { 274 | const content = /app-id=([^\s,]+)/.exec(meta.getAttribute('content')); 275 | 276 | appId = content && content[1] ? content[1] : appId; 277 | } 278 | 279 | this.setState({ 280 | appId, 281 | }); 282 | 283 | return appId; 284 | } 285 | 286 | retrieveInfo() { 287 | const link = 288 | `${this.props.url[this.state.type]}` || 289 | this.state.settings.getStoreLink() + this.state.appId; 290 | const inStore = ` 291 | ${this.props.price[this.state.type]} - ${ 292 | this.props.storeText[this.state.type] 293 | }`; 294 | let icon; 295 | 296 | if (isClient) { 297 | for (let i = 0, max = this.state.settings.iconRels.length; i < max; i++) { 298 | const rel = window.document.querySelector( 299 | `link[rel="${this.state.settings.iconRels[i]}"]` 300 | ); 301 | 302 | if (rel) { 303 | icon = rel.getAttribute('href'); 304 | break; 305 | } 306 | } 307 | } 308 | 309 | return { 310 | icon, 311 | link, 312 | inStore, 313 | }; 314 | } 315 | 316 | render() { 317 | if (!isClient) { 318 | return
; 319 | } 320 | 321 | // Don't show banner when: 322 | // 1) if device isn't iOS or Android 323 | // 2) website is loaded in app, 324 | // 3) user dismissed banner, 325 | // 4) or we have no app id in meta 326 | if ( 327 | !this.state.type || 328 | window.navigator.standalone || 329 | cookie.get('smartbanner-closed') || 330 | cookie.get('smartbanner-installed') 331 | ) { 332 | return
; 333 | } 334 | 335 | if (!this.state.appId) { 336 | return
; 337 | } 338 | 339 | this.show(); 340 | 341 | const { icon, link, inStore } = this.retrieveInfo(); 342 | const wrapperClassName = `smartbanner smartbanner-${ 343 | this.state.type 344 | } smartbanner-${this.props.position}`; 345 | const iconStyle = { 346 | backgroundImage: `url(${icon})`, 347 | }; 348 | 349 | return ( 350 |
351 |
352 | 355 | 356 |
357 |
{this.props.title}
358 |
{this.props.author}
359 |
{inStore}
360 |
361 | 372 |
373 |
374 | ); 375 | } 376 | } 377 | 378 | export default SmartBanner; 379 | -------------------------------------------------------------------------------- /src/components/__tests__/SmartBanner.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first, max-len, no-restricted-properties */ 2 | jest.mock('cookie-cutter', () => { 3 | return { 4 | set: jest.fn(), 5 | get: jest.fn(), 6 | }; 7 | }); 8 | 9 | import React from 'react'; 10 | import { mount } from 'enzyme'; 11 | import SmartBanner from '../SmartBanner'; 12 | 13 | // eslint-disable-next-line func-names 14 | describe('SmartBanner', function() { 15 | let cookie; 16 | 17 | beforeEach(() => { 18 | jest.resetModules(); 19 | 20 | cookie = require('cookie-cutter'); // eslint-disable-line global-require 21 | 22 | document.querySelector('head').innerHTML = ` 23 | 24 | 25 | 26 | 27 | `; 28 | document.body.innerHTML = '
'; 29 | 30 | this.params = { 31 | force: '', 32 | title: '', 33 | author: '', 34 | url: {}, 35 | }; 36 | 37 | this.makeSubject = (_props = {}) => { 38 | const props = { 39 | ...this.params, 40 | ..._props, 41 | }; 42 | 43 | return mount(, { 44 | attachTo: document.getElementById('root'), 45 | }); 46 | }; 47 | }); 48 | 49 | it('should be rendered', () => { 50 | const subject = this.makeSubject(); 51 | 52 | expect(subject.length).toBeTruthy(); 53 | }); 54 | 55 | describe('type snapshots', () => { 56 | it('should be matched the snapshot (no type)', () => { 57 | const subject = this.makeSubject(); 58 | 59 | expect(subject.html()).toMatchSnapshot(); 60 | }); 61 | 62 | it('should be matched the snapshot (android)', () => { 63 | const subject = this.makeSubject({ 64 | force: 'android', 65 | }); 66 | 67 | expect(subject.state('type')).toBe('android'); 68 | expect(subject.html()).toMatchSnapshot(); 69 | }); 70 | 71 | it('should be matched the snapshot (ios)', () => { 72 | const subject = this.makeSubject({ 73 | force: 'ios', 74 | }); 75 | 76 | expect(subject.state('type')).toBe('ios'); 77 | expect(subject.html()).toMatchSnapshot(); 78 | }); 79 | 80 | it('should be matched the snapshot (kindle)', () => { 81 | const subject = this.makeSubject({ 82 | force: 'kindle', 83 | }); 84 | 85 | expect(subject.state('type')).toBe('kindle'); 86 | expect(subject.html()).toMatchSnapshot(); 87 | }); 88 | 89 | it('should be matched the snapshot (windows)', () => { 90 | const subject = this.makeSubject({ 91 | force: 'windows', 92 | }); 93 | 94 | expect(subject.state('type')).toBe('windows'); 95 | expect(subject.html()).toMatchSnapshot(); 96 | }); 97 | }); 98 | 99 | describe('close smartbanner', () => { 100 | it('should change html classList and set cookie after invoke the close function', () => { 101 | const spy = jest.fn(); 102 | const subject = this.makeSubject({ 103 | force: 'android', 104 | onClose: spy, 105 | }); 106 | 107 | subject.instance().close(); 108 | 109 | expect(window.document.querySelector('html').classList).not.toContain( 110 | 'smartbanner-show' 111 | ); 112 | expect(cookie.set).toBeCalledWith('smartbanner-closed', 'true', { 113 | path: '/', 114 | expires: 'Fri, 26 May 2017 00:00:00 GMT', 115 | }); 116 | expect(spy).toHaveBeenCalled(); 117 | }); 118 | }); 119 | 120 | describe('click install on smartbanner', () => { 121 | it('should change html classList and set cookie after invoke the install function', () => { 122 | const spy = jest.fn(); 123 | const subject = this.makeSubject({ 124 | force: 'android', 125 | onInstall: spy, 126 | }); 127 | 128 | subject.instance().install(); 129 | 130 | expect(window.document.querySelector('html').classList).not.toContain( 131 | 'smartbanner-show' 132 | ); 133 | expect(cookie.set).toBeCalledWith('smartbanner-installed', 'true', { 134 | path: '/', 135 | expires: 'Thu, 24 Aug 2017 00:00:00 GMT', 136 | }); 137 | expect(spy).toHaveBeenCalled(); 138 | }); 139 | }); 140 | 141 | it('should only render div in SERVER env', () => { 142 | global.window = undefined; 143 | document.querySelector('head').innerHTML = ''; 144 | const subject = this.makeSubject(); 145 | 146 | expect(subject.html()).toBe('
'); 147 | }); 148 | 149 | it('should change type in state after receiving new force props', () => { 150 | const subject = this.makeSubject({ 151 | force: 'android', 152 | }); 153 | 154 | expect(subject.state('type')).toBe('android'); 155 | 156 | subject.setProps({ 157 | force: 'ios', 158 | }); 159 | 160 | expect(subject.state('type')).toBe('ios'); 161 | }); 162 | 163 | it('should remove html class names on unload', () => { 164 | const subject = this.makeSubject(); 165 | 166 | subject.unmount(); 167 | expect(window.document.querySelector('html').classList).not.toContain( 168 | 'smartbanner-show' 169 | ); 170 | expect(window.document.querySelector('html').classList).not.toContain( 171 | 'smartbanner-margin-top' 172 | ); 173 | expect(window.document.querySelector('html').classList).not.toContain( 174 | 'smartbanner-margin-bottom' 175 | ); 176 | }); 177 | 178 | describe('userAgent', () => { 179 | it('should change type to "ios" if we set iOS user agent', () => { 180 | window.navigator.__defineGetter__('userAgent', () => { 181 | return 'OperaMobile/12.02 (iPad; CPU OS 9_0 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 Mobile Safari/537.36'; 182 | }); 183 | 184 | const subject = this.makeSubject(); 185 | 186 | expect(subject.state('type')).toBe('ios'); 187 | }); 188 | 189 | it('should change type to "android" if we set android user agent', () => { 190 | window.navigator.__defineGetter__('userAgent', () => { 191 | return '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'; 192 | }); 193 | 194 | const subject = this.makeSubject(); 195 | 196 | expect(subject.state('type')).toBe('android'); 197 | }); 198 | 199 | it('should change type to "windows" if we set windows phone user agent', () => { 200 | window.navigator.__defineGetter__('userAgent', () => { 201 | return 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 810)'; 202 | }); 203 | 204 | const subject = this.makeSubject(); 205 | 206 | expect(subject.state('type')).toBe('windows'); 207 | }); 208 | 209 | it('should change type to "kindle" if we set kindle user agent', () => { 210 | window.navigator.__defineGetter__('userAgent', () => { 211 | return 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.1.0-80) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true'; 212 | }); 213 | 214 | const subject = this.makeSubject(); 215 | 216 | expect(subject.state('type')).toBe('kindle'); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/SmartBanner.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SmartBanner type snapshots should be matched the snapshot (android) 1`] = ` 4 | "
5 | Free - In Google Play
" 6 | `; 7 | 8 | exports[`SmartBanner type snapshots should be matched the snapshot (ios) 1`] = ` 9 | "
10 | Free - On the App Store
" 11 | `; 12 | 13 | exports[`SmartBanner type snapshots should be matched the snapshot (kindle) 1`] = ` 14 | "
15 | Free - In the Amazon Appstore
" 16 | `; 17 | 18 | exports[`SmartBanner type snapshots should be matched the snapshot (no type) 1`] = `"
"`; 19 | 20 | exports[`SmartBanner type snapshots should be matched the snapshot (windows) 1`] = ` 21 | "
22 | Free - In Windows Store
" 23 | `; 24 | -------------------------------------------------------------------------------- /src/example.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import SmartBanner from 'react-smartbanner'; // eslint-disable-line import/no-extraneous-dependencies 4 | import cookie from 'cookie-cutter'; 5 | 6 | class DemoComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | deviceType: '', 12 | position: 'top', 13 | }; 14 | } 15 | 16 | deleteCookie = () => { 17 | cookie.set('smartbanner-closed', null, { path: '/', expires: new Date(0) }); 18 | cookie.set('smartbanner-installed', null, { 19 | path: '/', 20 | expires: new Date(0), 21 | }); 22 | }; 23 | 24 | changeType(device) { 25 | this.setState({ 26 | deviceType: device, 27 | }); 28 | } 29 | 30 | changePosition(position) { 31 | this.setState({ 32 | position, 33 | }); 34 | } 35 | 36 | render() { 37 | const navButtonStyle = { 38 | margin: '20px 0 0 0', 39 | }; 40 | 41 | return ( 42 |
43 | 48 | 49 |
50 |
51 |
52 |
53 | 62 |
63 |
64 | 73 |
74 |
75 | 84 |
85 |
86 | 95 |
96 |
97 |
98 |
99 | 108 |
109 |
110 | 119 |
120 |
121 |
122 |
123 | 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | } 138 | 139 | ReactDOM.render(, document.getElementById('content')); 140 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patw0929/react-smartbanner/a45823ad085b6e1ee9f0f575fadf4931e7a02be7/src/icon.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React-SmartBanner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 29 | 30 | 31 |
32 |

If you can see this, something is broken (or JS is not enabled)!!.

33 |
34 | 35 |
36 |
37 |
38 | 41 | 42 |

Installation

43 | 44 |
npm install --save react-smartbanner
45 | 46 |

or...

47 | 48 |
yarn add react-smartbanner
49 | 50 |
51 | 52 |

Syntax

53 | 54 |

General

55 | 56 |

Remember to add following meta tags in your HTML page: (Use Facebook app as example)

57 | 58 |
<head>
 59 |   <meta name="apple-itunes-app" content="app-id=284882215">
 60 |   <meta name="google-play-app" content="app-id=com.facebook.katana">
 61 |   <meta name="msApplication-ID" content="82a23635-5bd9-df11-a844-00237de2db9e">
 62 |   <meta name="msApplication-PackageFamilyName" content="facebook_9wzdncrfhv5g">
 63 |   <meta name="kindle-fire-app" content="app-id=B0094BB4TW">
 64 | 
 65 |   <link rel="apple-touch-icon" href="icon.png">
 66 |   <link rel="android-touch-icon" href="icon.png">
 67 |   <link rel="windows-touch-icon" href="icon.png">
 68 | </head>
 69 | 
70 | 71 |

And React-SmartBanner component usage:

72 | 73 |

 74 | import React from 'react';
 75 | import ReactDOM from 'react-dom';
 76 | import SmartBanner from 'react-smartbanner';
 77 | import './node_modules/react-smartbanner/dist/main.css';
 78 | 
 79 | ReactDOM.render(<SmartBanner title={'Facebook'} />, document.getElementById('content'));
 80 | 
81 | 82 |
83 | 84 |

Props

85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 134 | 135 | 136 | 137 | 138 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 158 | 159 | 160 | 161 | 169 | 172 | 173 | 174 | 175 | 176 | 180 | 181 | 182 | 183 | 189 | 193 | 194 | 195 | 196 | 197 | 200 | 201 | 202 | 203 | 204 | 207 | 208 | 209 |
keydefaultdescription
daysHidden15Days to hide banner after close button is clicked.
daysReminder90Days to hide banner after "VIEW" button is clicked.
appStoreLanguage(user's browser language)Language code for the App Store.
title''App title.
author''App author.
button'View'Display on install button. (node)
storeText{ 129 | ios: 'On the App Store', 130 | android: 'In Google Play', 131 | windows: 'In Windows store', 132 | kindle: 'In Amazon Appstore' 133 | }Store text (object).
price{ 139 | ios: 'FREE', 140 | android: 'FREE', 141 | windows: 'FREE', 142 | kindle: 'FREE' 143 | }Price text (object).
position'top' / 'bottom'Display position on screen. Bottom is fixed, top scrolls out.
force'' 155 | Force to display in specific device.
156 | (android, ios, windows, kindle) 157 |
url 162 | { 163 | ios: 'http://www.domain.com', 164 | android: 'http://www.domain2.com', 165 | windows: 'http://www.domain3.com', 166 | kindle: 'http://www.domain4.com' 167 | } 168 | 170 | Custom URL for each device 171 |
ignoreIosVersionfalse 177 | Boolean to ignore the iOS version, so that the banner is also displayed on 178 | devices that support the native banner. 179 |
appMeta{ 184 | ios: 'apple-itunes-app', 185 | android: 'google-play-app', 186 | windows: 'msApplication-ID', 187 | kindle: 'kindle-fire-app' 188 | } 190 | The custom meta tag name (object).
191 | It provide an option to enforce using custom meta tag to show js react-smartbanner for newer iOS versions. 192 |
onCloseNo default value 198 | Optional callback when user clicks on close button. 199 |
onInstallNo default value 205 | Optional callback when user clicks on install button. 206 |
210 |
211 | 212 |
213 | 214 |

Based on

215 | 216 | 219 | 220 |
221 | 222 |

Lincense

223 | 224 |

MIT License

225 |
226 |
227 | 235 |
236 | 237 |
238 |
239 | Fork me on GitHub 240 |
241 |
242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /src/styles/style.scss: -------------------------------------------------------------------------------- 1 | $smartbanner-close-margin: 5px; 2 | $smartbanner-close-size: 17px; 3 | $smartbanner-icon-margin: 12px; 4 | $smartbanner-icon-size: 57px; 5 | $smartbanner-button-size: 110px; 6 | 7 | .smartbanner-show { 8 | &.smartbanner-margin-top { 9 | margin-top: 80px; 10 | } 11 | 12 | &.smartbanner-margin-bottom { 13 | margin-bottom: 80px; 14 | } 15 | 16 | .smartbanner { 17 | display: block; 18 | } 19 | } 20 | 21 | /** Default **/ 22 | .smartbanner { 23 | left: 0; 24 | display: none; 25 | width: 100%; 26 | height: 80px; 27 | line-height: 80px; 28 | font-family: 'Helvetica Neue', sans-serif; 29 | background: #f4f4f4; 30 | z-index: 9998; 31 | -webkit-font-smoothing: antialiased; 32 | overflow: hidden; 33 | -webkit-text-size-adjust: none; 34 | } 35 | 36 | .smartbanner-top { 37 | position: absolute; 38 | top: 0; 39 | } 40 | 41 | .smartbanner-bottom { 42 | position: fixed; 43 | bottom: 0; 44 | } 45 | 46 | .smartbanner-container { 47 | margin: 0 auto; 48 | padding: 0 5px; 49 | } 50 | 51 | .smartbanner-close { 52 | display: inline-block; 53 | vertical-align: middle; 54 | margin: 0 $smartbanner-close-margin 0 0; 55 | font-family: 'ArialRoundedMTBold', Arial; 56 | font-size: 20px; 57 | text-align: center; 58 | color: #888; 59 | text-decoration: none; 60 | border: 0; 61 | border-radius: 14px; 62 | padding: 0 0 1px; 63 | background-color: transparent; 64 | -webkit-font-smoothing: subpixel-antialiased; 65 | 66 | &:active, 67 | &:hover { 68 | color: #aaa; 69 | } 70 | } 71 | 72 | .smartbanner-icon { 73 | display: inline-block; 74 | vertical-align: middle; 75 | width: $smartbanner-icon-size; 76 | height: $smartbanner-icon-size; 77 | margin-right: $smartbanner-icon-margin; 78 | background-size: cover; 79 | border-radius: 10px; 80 | } 81 | 82 | .smartbanner-info { 83 | white-space: normal; 84 | display: inline-block; 85 | vertical-align: middle; 86 | width: calc(99% - #{$smartbanner-close-margin} - #{$smartbanner-close-size} - #{$smartbanner-icon-margin} - #{$smartbanner-icon-size} - #{$smartbanner-button-size}); 87 | font-size: 11px; 88 | line-height: 1.2em; 89 | font-weight: bold; 90 | } 91 | 92 | .smartbanner-wrapper { 93 | max-width: $smartbanner-button-size; 94 | display: inline-block; 95 | text-align: right; 96 | width: 100%; 97 | } 98 | 99 | .smartbanner-title { 100 | font-size: 13px; 101 | line-height: 18px; 102 | text-overflow: ellipsis; 103 | white-space: nowrap; 104 | overflow: hidden; 105 | } 106 | 107 | .smartbanner-description { 108 | max-height: 40px; 109 | overflow: hidden; 110 | } 111 | 112 | .smartbanner-author { 113 | text-overflow: ellipsis; 114 | white-space: nowrap; 115 | overflow: hidden; 116 | 117 | &:empty { 118 | + .smartbanner-description { 119 | max-height: 50px; 120 | } 121 | } 122 | } 123 | 124 | .smartbanner-button { 125 | margin: auto 0; 126 | height: 24px; 127 | font-size: 14px; 128 | line-height: 24px; 129 | text-align: center; 130 | font-weight: bold; 131 | color: #6a6a6a; 132 | text-transform: uppercase; 133 | text-decoration: none; 134 | display: inline-block; 135 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); 136 | 137 | &:active, 138 | &:hover { 139 | color: #aaa; 140 | } 141 | } 142 | 143 | /** iOS **/ 144 | .smartbanner-ios { 145 | background: #f2f2f2; 146 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 147 | line-height: 80px; 148 | 149 | .smartbanner-close { 150 | border: 0; 151 | width: 18px; 152 | height: 18px; 153 | line-height: 18px; 154 | font-family: Arial; 155 | color: #888; 156 | text-shadow: 0 1px 0 #fff; 157 | -webkit-font-smoothing: none; 158 | 159 | &:active, 160 | &:hover { 161 | color: #888; 162 | } 163 | } 164 | 165 | .smartbanner-icon { 166 | background-size: cover; 167 | } 168 | 169 | .smartbanner-info { 170 | color: #6a6a6a; 171 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); 172 | font-weight: 300; 173 | } 174 | 175 | .smartbanner-title { 176 | color: #4d4d4d; 177 | font-weight: 500; 178 | } 179 | 180 | .smartbanner-button { 181 | padding: 0 10px; 182 | font-size: 15px; 183 | min-width: 10%; 184 | font-weight: 400; 185 | color: #0c71fd; 186 | 187 | &:active, 188 | &:hover { 189 | background: #f2f2f2; 190 | } 191 | } 192 | } 193 | 194 | /** Android **/ 195 | .smartbanner-android { 196 | background: #3d3d3d url('data:image/gif;base64,R0lGODlhCAAIAIABAFVVVf///yH5BAEHAAEALAAAAAAIAAgAAAINRG4XudroGJBRsYcxKAA7'); 197 | box-shadow: inset 0 4px 0 #88b131; 198 | line-height: 82px; 199 | 200 | .smartbanner-close { 201 | border: 0; 202 | max-width: $smartbanner-close-size; 203 | width: 100%; 204 | height: $smartbanner-close-size; 205 | line-height: $smartbanner-close-size; 206 | margin-right: 7px; 207 | color: #b1b1b3; 208 | background: #1c1e21; 209 | text-shadow: 0 1px 1px #000; 210 | text-decoration: none; 211 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.8) inset, 0 1px 1px rgba(255, 255, 255, 0.3); 212 | cursor: pointer; 213 | 214 | &:active, 215 | &:hover { 216 | color: #eee; 217 | } 218 | } 219 | 220 | .smartbanner-icon { 221 | background-color: transparent; 222 | box-shadow: none; 223 | } 224 | 225 | .smartbanner-info { 226 | color: #ccc; 227 | text-shadow: 0 1px 2px #000; 228 | } 229 | 230 | .smartbanner-title { 231 | color: #fff; 232 | font-weight: bold; 233 | } 234 | 235 | .smartbanner-button { 236 | min-width: 12%; 237 | color: #d1d1d1; 238 | font-weight: bold; 239 | padding: 0; 240 | background: none; 241 | border-radius: 0; 242 | box-shadow: 0 0 0 1px #333, 0 0 0 2px #dddcdc; 243 | 244 | &:active, 245 | &:hover { 246 | background: none; 247 | } 248 | } 249 | 250 | .smartbanner-button-text { 251 | text-align: center; 252 | display: block; 253 | padding: 0 10px; 254 | background: #42b6c9; 255 | background: linear-gradient(to bottom, #42b6c9, #39a9bb); // sass-lint:disable-block no-duplicate-properties 256 | text-transform: none; 257 | text-shadow: none; 258 | box-shadow: none; 259 | 260 | &:active, 261 | &:hover { 262 | background: #2ac7e1; 263 | } 264 | } 265 | } 266 | 267 | /** Windows / Kindle **/ 268 | .smartbanner-windows, 269 | .smartbanner-kindle { 270 | background: #f4f4f4; 271 | background: linear-gradient(to bottom, #f4f4f4, #cdcdcd); // sass-lint:disable-block no-duplicate-properties 272 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); 273 | line-height: 80px; 274 | 275 | .smartbanner-close { 276 | border: 0; 277 | width: 18px; 278 | height: 18px; 279 | line-height: 18px; 280 | color: #888; 281 | text-shadow: 0 1px 0 #fff; 282 | 283 | &:active, 284 | &:hover { 285 | color: #aaa; 286 | } 287 | } 288 | 289 | .smartbanner-icon { 290 | background: rgba(0, 0, 0, 0.6); 291 | background-size: cover; 292 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 293 | } 294 | 295 | .smartbanner-info { 296 | color: #6a6a6a; 297 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); 298 | } 299 | 300 | .smartbanner-title, 301 | .smartbanner-title { 302 | color: #4d4d4d; 303 | font-weight: bold; 304 | } 305 | 306 | .smartbanner-button { 307 | padding: 0 10px; 308 | min-width: 10%; 309 | color: #6a6a6a; 310 | background: #efefef; 311 | background: linear-gradient(to bottom, #efefef, #dcdcdc); // sass-lint:disable-block no-duplicate-properties 312 | border-radius: 3px; 313 | box-shadow: inset 0 0 0 1px #bfbfbf, 314 | 0 1px 0 rgba(255, 255, 255, 0.6), 315 | 0 2px 0 rgba(255, 255, 255, 0.7) inset; 316 | 317 | &:active, 318 | &:hover { 319 | background: #dcdcdc; 320 | background: linear-gradient(to bottom, #dcdcdc, #efefef); // sass-lint:disable-block no-duplicate-properties 321 | } 322 | } 323 | } 324 | --------------------------------------------------------------------------------