├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bundle.js ├── docs ├── bundle.js └── index.html ├── package.json ├── rollup.config.js └── src ├── Context.js ├── UserAgent.js ├── index.js ├── launcher ├── Android.js ├── Base.js ├── BrowserBackgroundObserver.js ├── IOS.js ├── PC.js └── index.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015-rollup", "stage-2" ] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "no-underscore-dangle": 0, 6 | "no-unused-expressions": 0, 7 | "no-inner-declarations": 0, 8 | "no-use-before-define": 0, 9 | "no-undef": 0, 10 | "no-param-reassign": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | tmp 5 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 kwst 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 | # fallback-custom-scheme 2 | 3 | Launcher application (iOS, Android, Windows, Mac) from web page. 4 | Use custom url scheme to launch app. 5 | If not installed the application, fallback (ex. app store, google play store etc). 6 | 7 | | app | handling | 8 | | --- | --- | 9 | | installed | open application | 10 | | not installed | fallback (ex. your store url) | 11 | 12 | ### Demo 13 | [demo page](https://satoshikawabata.github.io/fallback-custom-scheme/) 14 | 15 | ## Install 16 | ### npm 17 | ```shell 18 | npm i fallback-custom-scheme 19 | ``` 20 | 21 | ### download from url 22 | ``` 23 | https://satoshikawabata.github.io/fallback-custom-scheme/bundle.js 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | var FallbackCustomScheme = requie('fallback-custom-scheme'); 30 | var fcs = new window.FallbackCustomScheme({ 31 | urlScheme: 'hoge://temp', // your application custom scheme 32 | fallback: 'itunes://hoge', // if not installed the applicaiton, handling (ex. app store, google play store etc) 33 | onFallback: function(){}, // fallback handler 34 | browserback: 'back', // handling on return browser 35 | onBrowserback: function(){}, // browser back handler 36 | query: { // query 37 | a: aaa, 38 | b: bbb 39 | } 40 | }); 41 | ``` 42 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.FallbackCustomScheme = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var classCallCheck = function (instance, Constructor) { 8 | if (!(instance instanceof Constructor)) { 9 | throw new TypeError("Cannot call a class as a function"); 10 | } 11 | }; 12 | 13 | var createClass = function () { 14 | function defineProperties(target, props) { 15 | for (var i = 0; i < props.length; i++) { 16 | var descriptor = props[i]; 17 | descriptor.enumerable = descriptor.enumerable || false; 18 | descriptor.configurable = true; 19 | if ("value" in descriptor) descriptor.writable = true; 20 | Object.defineProperty(target, descriptor.key, descriptor); 21 | } 22 | } 23 | 24 | return function (Constructor, protoProps, staticProps) { 25 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 26 | if (staticProps) defineProperties(Constructor, staticProps); 27 | return Constructor; 28 | }; 29 | }(); 30 | 31 | var inherits = function (subClass, superClass) { 32 | if (typeof superClass !== "function" && superClass !== null) { 33 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 34 | } 35 | 36 | subClass.prototype = Object.create(superClass && superClass.prototype, { 37 | constructor: { 38 | value: subClass, 39 | enumerable: false, 40 | writable: true, 41 | configurable: true 42 | } 43 | }); 44 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 45 | }; 46 | 47 | var possibleConstructorReturn = function (self, call) { 48 | if (!self) { 49 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 50 | } 51 | 52 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 53 | }; 54 | 55 | var RE_IS_IPHONE = /(iPhone\sOS)\s([\d_]+)/; 56 | var RE_IS_IPAD = /(iPad).*OS\s([\d_]+)/; 57 | var RE_IS_IPOD_TOUCH = /(iPod touch).*OS\s([\d_]+)/; 58 | var RE_IS_ANDROID = /(Android)\s+([\d.]+)/; 59 | var RE_IS_WIN = /Windows/; 60 | var RE_IS_MAC = /Mac OS/; 61 | 62 | var UserAgent = function UserAgent() { 63 | classCallCheck(this, UserAgent); 64 | this.isIOS = null; 65 | this.isAndroid = null; 66 | this.isWin = null; 67 | this.isMac = null; 68 | this.isPC = null; 69 | this.osVer = null; 70 | 71 | var ua = navigator.userAgent; 72 | this.isIOS = !!ua.match(RE_IS_IPHONE) || ua.match(RE_IS_IPAD) || ua.match(RE_IS_IPOD_TOUCH); 73 | this.isAndroid = !!ua.match(RE_IS_ANDROID); 74 | this.isWin = !!ua.match(RE_IS_WIN); 75 | this.isMac = !!ua.match(RE_IS_MAC); 76 | this.isPC = !!(this.isWin || this.isMac); 77 | if (this.isIOS || this.isAndroid) { 78 | var ver = ua.match(/(OS|Android) ([0-9_\.]+){1,3}/); 79 | ver = ver && ver[2].replace(/_/g, '.'); 80 | this.osVer = ver && Number(ver[0]); 81 | } else { 82 | this.osVer = null; 83 | } 84 | }; 85 | 86 | function createCommonjsModule(fn, module) { 87 | return module = { exports: {} }, fn(module, module.exports), module.exports; 88 | } 89 | 90 | var __moduleExports = createCommonjsModule(function (module) { 91 | 'use strict'; 92 | 93 | module.exports = function (str) { 94 | return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { 95 | return '%' + c.charCodeAt(0).toString(16).toUpperCase(); 96 | }); 97 | }; 98 | }); 99 | 100 | var __moduleExports$1 = createCommonjsModule(function (module) { 101 | 'use strict'; 102 | /* eslint-disable no-unused-vars */ 103 | 104 | var hasOwnProperty = Object.prototype.hasOwnProperty; 105 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 106 | 107 | function toObject(val) { 108 | if (val === null || val === undefined) { 109 | throw new TypeError('Object.assign cannot be called with null or undefined'); 110 | } 111 | 112 | return Object(val); 113 | } 114 | 115 | function shouldUseNative() { 116 | try { 117 | if (!Object.assign) { 118 | return false; 119 | } 120 | 121 | // Detect buggy property enumeration order in older V8 versions. 122 | 123 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 124 | var test1 = new String('abc'); // eslint-disable-line 125 | test1[5] = 'de'; 126 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 127 | return false; 128 | } 129 | 130 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 131 | var test2 = {}; 132 | for (var i = 0; i < 10; i++) { 133 | test2['_' + String.fromCharCode(i)] = i; 134 | } 135 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 136 | return test2[n]; 137 | }); 138 | if (order2.join('') !== '0123456789') { 139 | return false; 140 | } 141 | 142 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 143 | var test3 = {}; 144 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 145 | test3[letter] = letter; 146 | }); 147 | if (Object.keys(Object.assign({}, test3)).join('') !== 'abcdefghijklmnopqrst') { 148 | return false; 149 | } 150 | 151 | return true; 152 | } catch (e) { 153 | // We don't expect any of the above to throw, but better to be safe. 154 | return false; 155 | } 156 | } 157 | 158 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 159 | var from; 160 | var to = toObject(target); 161 | var symbols; 162 | 163 | for (var s = 1; s < arguments.length; s++) { 164 | from = Object(arguments[s]); 165 | 166 | for (var key in from) { 167 | if (hasOwnProperty.call(from, key)) { 168 | to[key] = from[key]; 169 | } 170 | } 171 | 172 | if (Object.getOwnPropertySymbols) { 173 | symbols = Object.getOwnPropertySymbols(from); 174 | for (var i = 0; i < symbols.length; i++) { 175 | if (propIsEnumerable.call(from, symbols[i])) { 176 | to[symbols[i]] = from[symbols[i]]; 177 | } 178 | } 179 | } 180 | } 181 | 182 | return to; 183 | }; 184 | }); 185 | 186 | var index = createCommonjsModule(function (module, exports) { 187 | 'use strict'; 188 | 189 | var strictUriEncode = __moduleExports; 190 | var objectAssign = __moduleExports$1; 191 | 192 | function encode(value, opts) { 193 | if (opts.encode) { 194 | return opts.strict ? strictUriEncode(value) : encodeURIComponent(value); 195 | } 196 | 197 | return value; 198 | } 199 | 200 | exports.extract = function (str) { 201 | return str.split('?')[1] || ''; 202 | }; 203 | 204 | exports.parse = function (str) { 205 | // Create an object with no prototype 206 | // https://github.com/sindresorhus/query-string/issues/47 207 | var ret = Object.create(null); 208 | 209 | if (typeof str !== 'string') { 210 | return ret; 211 | } 212 | 213 | str = str.trim().replace(/^(\?|#|&)/, ''); 214 | 215 | if (!str) { 216 | return ret; 217 | } 218 | 219 | str.split('&').forEach(function (param) { 220 | var parts = param.replace(/\+/g, ' ').split('='); 221 | // Firefox (pre 40) decodes `%3D` to `=` 222 | // https://github.com/sindresorhus/query-string/pull/37 223 | var key = parts.shift(); 224 | var val = parts.length > 0 ? parts.join('=') : undefined; 225 | 226 | key = decodeURIComponent(key); 227 | 228 | // missing `=` should be `null`: 229 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters 230 | val = val === undefined ? null : decodeURIComponent(val); 231 | 232 | if (ret[key] === undefined) { 233 | ret[key] = val; 234 | } else if (Array.isArray(ret[key])) { 235 | ret[key].push(val); 236 | } else { 237 | ret[key] = [ret[key], val]; 238 | } 239 | }); 240 | 241 | return ret; 242 | }; 243 | 244 | exports.stringify = function (obj, opts) { 245 | var defaults = { 246 | encode: true, 247 | strict: true 248 | }; 249 | 250 | opts = objectAssign(defaults, opts); 251 | 252 | return obj ? Object.keys(obj).sort().map(function (key) { 253 | var val = obj[key]; 254 | 255 | if (val === undefined) { 256 | return ''; 257 | } 258 | 259 | if (val === null) { 260 | return encode(key, opts); 261 | } 262 | 263 | if (Array.isArray(val)) { 264 | var result = []; 265 | 266 | val.slice().forEach(function (val2) { 267 | if (val2 === undefined) { 268 | return; 269 | } 270 | 271 | if (val2 === null) { 272 | result.push(encode(key, opts)); 273 | } else { 274 | result.push(encode(key, opts) + '=' + encode(val2, opts)); 275 | } 276 | }); 277 | 278 | return result.join('&'); 279 | } 280 | 281 | return encode(key, opts) + '=' + encode(val, opts); 282 | }).filter(function (x) { 283 | return x.length > 0; 284 | }).join('&') : ''; 285 | }; 286 | }); 287 | 288 | var Context = function () { 289 | function Context(context) { 290 | classCallCheck(this, Context); 291 | this._urlScheme = null; 292 | this.query = null; 293 | this.fallback = null; 294 | this.onFallback = null; 295 | this.browserback = null; 296 | this.onBrowserback = null; 297 | 298 | context && this.extend(context); 299 | } 300 | 301 | createClass(Context, [{ 302 | key: 'extend', 303 | value: function extend(target) { 304 | target.urlScheme && (this._urlScheme = target.urlScheme); 305 | target.query && (this.query = target.query); 306 | target.fallback && (this.fallback = target.fallback); 307 | target.onFallback && (this.onFallback = target.onFallback); 308 | target.browserback && (this.browserback = target.browserback); 309 | target.onBrowserback && (this.onBrowserback = target.onBrowserback); 310 | } 311 | }, { 312 | key: 'handleFallback', 313 | value: function handleFallback() { 314 | var fallback = this.fallback; 315 | 316 | if (fallback) { 317 | location.href = fallback; 318 | } 319 | this.onFallback && this.onFallback(); 320 | } 321 | }, { 322 | key: 'handleBrowserBack', 323 | value: function handleBrowserBack() { 324 | var browserback = this.browserback; 325 | 326 | if (browserback === 'back') { 327 | history.back(); 328 | } 329 | this.onBrowserback && this.onBrowserback(); 330 | } 331 | }, { 332 | key: 'urlScheme', 333 | get: function get() { 334 | var query = this.query; 335 | var _urlScheme = this._urlScheme; 336 | 337 | var fullPath = _urlScheme; 338 | if (query) { 339 | var queryStr = index.stringify(query); 340 | var parsed = new URL(fullPath); 341 | parsed.search += (parsed.search ? '&' : '?') + queryStr; 342 | fullPath = parsed.href; 343 | } 344 | 345 | return fullPath; 346 | } 347 | }]); 348 | return Context; 349 | }(); 350 | 351 | var _window = window; 352 | var document$1 = _window.document; 353 | var location$1 = _window.location; 354 | 355 | 356 | var openURLByIFrame = function openURLByIFrame(url) { 357 | var iframe = document$1.createElement('iframe'); 358 | iframe.src = url; 359 | iframe.setAttribute('height', 0); 360 | iframe.setAttribute('width', 0); 361 | iframe.style.display = 'none'; 362 | document$1.body.appendChild(iframe); 363 | return iframe; 364 | }; 365 | 366 | var openURLByHref = function openURLByHref(url, isReplace) { 367 | if (isReplace) { 368 | location$1.replace(url); 369 | } else { 370 | location$1.href = url; 371 | } 372 | }; 373 | 374 | var parseQuery = function parseQuery(str) { 375 | var queries = {}; 376 | var queryStr = str.replace(/^(\?|#|&)/, ''); 377 | var elements = queryStr.split('&'); 378 | for (var i = 0; i < elements.length; i++) { 379 | var param = elements[i]; 380 | var parts = param.replace(/\+/g, ' ').split('='); 381 | var key = parts.shift(); 382 | var val = parts.length > 0 ? parts.join('=') : undefined; 383 | key = decodeURIComponent(key); 384 | val = val === undefined ? null : decodeURIComponent(val); 385 | 386 | if (queries[key] === undefined) { 387 | queries[key] = val; 388 | } else if (Array.isArray(queries[key])) { 389 | queries[key].push(val); 390 | } else { 391 | queries[key] = [queries[key], val]; 392 | } 393 | } 394 | 395 | return queries; 396 | }; 397 | 398 | var util$1 = { 399 | openURLByIFrame: openURLByIFrame, 400 | openURLByHref: openURLByHref, 401 | parseQuery: parseQuery 402 | }; 403 | 404 | var BrowserBackgroundObserver = function () { 405 | function BrowserBackgroundObserver(onReturn, onLeave, useRequestAnimationFrame) { 406 | var _this = this; 407 | 408 | classCallCheck(this, BrowserBackgroundObserver); 409 | this.isReturnBrowser = false; 410 | this.isLeaveBrowser = false; 411 | 412 | this.onReturn = onReturn; 413 | this.onLeave = onLeave; 414 | this.useRequestAnimationFrame = useRequestAnimationFrame; 415 | 416 | this._onLeave = this._onLeave.bind(this); 417 | this._onBlur = this._onBlur.bind(this); 418 | this._onFocus = this._onFocus.bind(this); 419 | this._onVisibilityChange = this._onVisibilityChange.bind(this); 420 | this._onWebkitVisibilityChange = this._onWebkitVisibilityChange.bind(this); 421 | 422 | window.focus(); 423 | window.addEventListener('blur', this._onBlur); 424 | if (document.hidden !== undefined) { 425 | document.addEventListener('visibilitychange', this._onVisibilityChange); 426 | } else if (document.webkitHidden !== undefined) { 427 | document.addEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 428 | } 429 | 430 | if (useRequestAnimationFrame) { 431 | (function () { 432 | var observe = function observe() { 433 | _this.rafId = window.requestAnimationFrame(observe); 434 | if (_this.rafTimer !== false) { 435 | clearTimeout(_this.rafTimer); 436 | } 437 | 438 | _this.rafTimer = setTimeout(function () { 439 | if (!isReturnBrowser) { 440 | _onReturn(); 441 | } 442 | 443 | window.cancelAnimationFrame(_this.rafId); 444 | }, 1000); 445 | }; 446 | 447 | observe(); 448 | })(); 449 | } 450 | } 451 | 452 | createClass(BrowserBackgroundObserver, [{ 453 | key: '_onReturn', 454 | value: function _onReturn() { 455 | var rafTimer = this.rafTimer; 456 | var rafId = this.rafId; 457 | 458 | this.isReturnBrowser = true; 459 | document.removeEventListener('visibilitychange', this._onVisibilityChange); 460 | document.removeEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 461 | window.removeEventListener('focus', this._onFocus); 462 | clearTimeout(rafTimer); 463 | window.cancelAnimationFrame && window.cancelAnimationFrame(rafId); 464 | 465 | this.onReturn && this.onReturn(); 466 | } 467 | }, { 468 | key: '_onLeave', 469 | value: function _onLeave() { 470 | this.isLeaveBrowser = true; 471 | this.onLeave && this.onLeave(); 472 | } 473 | }, { 474 | key: '_onBlur', 475 | value: function _onBlur() { 476 | var isLeaveBrowser = this.isLeaveBrowser; 477 | 478 | window.removeEventListener('blur', this._onBlur); 479 | window.addEventListener('focus', this._onFocus); 480 | if (!isLeaveBrowser) { 481 | this._onLeave(); 482 | } 483 | } 484 | }, { 485 | key: '_onFocus', 486 | value: function _onFocus() { 487 | var isReturnBrowser = this.isReturnBrowser; 488 | 489 | if (!isReturnBrowser) { 490 | this._onReturn(); 491 | } 492 | } 493 | }, { 494 | key: '_onVisibilityChange', 495 | value: function _onVisibilityChange() { 496 | var isLeaveBrowser = this.isLeaveBrowser; 497 | var isReturnBrowser = this.isReturnBrowser; 498 | 499 | if (!isLeaveBrowser && document.hidden) { 500 | this._onLeave(); 501 | } else if (!isReturnBrowser && !document.hidden) { 502 | this._onReturn(); 503 | } 504 | } 505 | }, { 506 | key: '_onWebkitVisibilityChange', 507 | value: function _onWebkitVisibilityChange() { 508 | var isLeaveBrowser = this.isLeaveBrowser; 509 | var isReturnBrowser = this.isReturnBrowser; 510 | 511 | if (!isLeaveBrowser && document.webkitHidden) { 512 | this._onLeave(); 513 | } else if (!isReturnBrowser && !document.webkitHidden) { 514 | this._onReturn(); 515 | } 516 | } 517 | }]); 518 | return BrowserBackgroundObserver; 519 | }(); 520 | 521 | var BaseLauncher = function () { 522 | function BaseLauncher(context) { 523 | classCallCheck(this, BaseLauncher); 524 | this._context = null; 525 | this._fallbackTime = 500; 526 | 527 | this._context = context; 528 | 529 | this._handleTimeout = this._handleTimeout.bind(this); 530 | this._handleLeaveBrowser = this._handleLeaveBrowser.bind(this); 531 | this._handleBrowserBack = this._handleBrowserBack.bind(this); 532 | } 533 | 534 | createClass(BaseLauncher, [{ 535 | key: 'launch', 536 | value: function launch() { 537 | util$1.openURLByIFrame(this._context.urlScheme); 538 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 539 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 540 | } 541 | }, { 542 | key: '_handleTimeout', 543 | value: function _handleTimeout() { 544 | this._context.handleFallback(); 545 | } 546 | }, { 547 | key: '_handleLeaveBrowser', 548 | value: function _handleLeaveBrowser() { 549 | var _timerId = this._timerId; 550 | 551 | clearTimeout(_timerId); 552 | } 553 | }, { 554 | key: '_handleBrowserBack', 555 | value: function _handleBrowserBack() { 556 | this._context.handleBrowserBack(); 557 | } 558 | }]); 559 | return BaseLauncher; 560 | }(); 561 | 562 | var IOS = function (_BaseLauncher) { 563 | inherits(IOS, _BaseLauncher); 564 | 565 | function IOS() { 566 | classCallCheck(this, IOS); 567 | return possibleConstructorReturn(this, (IOS.__proto__ || Object.getPrototypeOf(IOS)).apply(this, arguments)); 568 | } 569 | 570 | createClass(IOS, [{ 571 | key: 'launch', 572 | value: function launch() { 573 | var _context = this._context; 574 | 575 | if (_context.osVer > 8) { 576 | util$1.openURLByHref(_context.urlScheme); 577 | } else { 578 | util$1.openURLByIFrame(_context.urlScheme); 579 | } 580 | 581 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 582 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 583 | } 584 | }]); 585 | return IOS; 586 | }(BaseLauncher); 587 | 588 | var Android = function (_BaseLauncher) { 589 | inherits(Android, _BaseLauncher); 590 | 591 | function Android() { 592 | classCallCheck(this, Android); 593 | return possibleConstructorReturn(this, (Android.__proto__ || Object.getPrototypeOf(Android)).apply(this, arguments)); 594 | } 595 | 596 | return Android; 597 | }(BaseLauncher); 598 | 599 | var PC = function (_BaseLauncher) { 600 | inherits(PC, _BaseLauncher); 601 | 602 | function PC(context) { 603 | classCallCheck(this, PC); 604 | 605 | var _this = possibleConstructorReturn(this, (PC.__proto__ || Object.getPrototypeOf(PC)).call(this, context)); 606 | 607 | _this._enableEventListener(window); 608 | _this._enableEventListener(document); 609 | return _this; 610 | } 611 | 612 | createClass(PC, [{ 613 | key: 'launch', 614 | value: function launch() { 615 | util$1.openURLByIFrame(this._context.urlScheme); 616 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 617 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 618 | } 619 | }, { 620 | key: '_enableEventListener', 621 | value: function _enableEventListener(target) { 622 | if (!target.addEventListener) { 623 | target.addEventListener = function (type, callback) { 624 | target.attachEvent('on' + type, callback); 625 | }; 626 | } 627 | 628 | if (!target.removeEventListener) { 629 | target.removeEventListener = function (type, callback) { 630 | target.detachEvent('on' + type, callback); 631 | }; 632 | } 633 | } 634 | }]); 635 | return PC; 636 | }(BaseLauncher); 637 | 638 | var util = { 639 | IOS: IOS, 640 | Android: Android, 641 | PC: PC 642 | }; 643 | 644 | var FallbackCustomScheme = function () { 645 | function FallbackCustomScheme(options) { 646 | classCallCheck(this, FallbackCustomScheme); 647 | this._ua = new UserAgent(); 648 | 649 | this._context = new Context(options); 650 | } 651 | 652 | createClass(FallbackCustomScheme, [{ 653 | key: 'launch', 654 | value: function launch(options) { 655 | var _context = this._context; 656 | var _ua = this._ua; 657 | 658 | options && _context.extend(options); 659 | var urlScheme = _context.urlScheme; 660 | var isIOS = _ua.isIOS; 661 | var isAndroid = _ua.isAndroid; 662 | var isPC = _ua.isPC; 663 | 664 | 665 | if (!urlScheme) { 666 | throw new Error('you must set "urlScheme"'); 667 | } 668 | 669 | if (isIOS) { 670 | this._launcher = new IOS(_context); 671 | } else if (isAndroid) { 672 | this._launcher = new Android(_context); 673 | } else if (isPC) { 674 | this._launcher = new PC(_context); 675 | } 676 | if (this._launcher) { 677 | this._launcher.launch(); 678 | } 679 | } 680 | }]); 681 | return FallbackCustomScheme; 682 | }(); 683 | 684 | return FallbackCustomScheme; 685 | 686 | }))); -------------------------------------------------------------------------------- /docs/bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.FallbackCustomScheme = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var classCallCheck = function (instance, Constructor) { 8 | if (!(instance instanceof Constructor)) { 9 | throw new TypeError("Cannot call a class as a function"); 10 | } 11 | }; 12 | 13 | var createClass = function () { 14 | function defineProperties(target, props) { 15 | for (var i = 0; i < props.length; i++) { 16 | var descriptor = props[i]; 17 | descriptor.enumerable = descriptor.enumerable || false; 18 | descriptor.configurable = true; 19 | if ("value" in descriptor) descriptor.writable = true; 20 | Object.defineProperty(target, descriptor.key, descriptor); 21 | } 22 | } 23 | 24 | return function (Constructor, protoProps, staticProps) { 25 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 26 | if (staticProps) defineProperties(Constructor, staticProps); 27 | return Constructor; 28 | }; 29 | }(); 30 | 31 | var inherits = function (subClass, superClass) { 32 | if (typeof superClass !== "function" && superClass !== null) { 33 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 34 | } 35 | 36 | subClass.prototype = Object.create(superClass && superClass.prototype, { 37 | constructor: { 38 | value: subClass, 39 | enumerable: false, 40 | writable: true, 41 | configurable: true 42 | } 43 | }); 44 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 45 | }; 46 | 47 | var possibleConstructorReturn = function (self, call) { 48 | if (!self) { 49 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 50 | } 51 | 52 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 53 | }; 54 | 55 | var RE_IS_IPHONE = /(iPhone\sOS)\s([\d_]+)/; 56 | var RE_IS_IPAD = /(iPad).*OS\s([\d_]+)/; 57 | var RE_IS_IPOD_TOUCH = /(iPod touch).*OS\s([\d_]+)/; 58 | var RE_IS_ANDROID = /(Android)\s+([\d.]+)/; 59 | var RE_IS_WIN = /Windows/; 60 | var RE_IS_MAC = /Mac OS/; 61 | 62 | var UserAgent = function UserAgent() { 63 | classCallCheck(this, UserAgent); 64 | this.isIOS = null; 65 | this.isAndroid = null; 66 | this.isWin = null; 67 | this.isMac = null; 68 | this.isPC = null; 69 | this.osVer = null; 70 | 71 | var ua = navigator.userAgent; 72 | this.isIOS = !!ua.match(RE_IS_IPHONE) || ua.match(RE_IS_IPAD) || ua.match(RE_IS_IPOD_TOUCH); 73 | this.isAndroid = !!ua.match(RE_IS_ANDROID); 74 | this.isWin = !!ua.match(RE_IS_WIN); 75 | this.isMac = !!ua.match(RE_IS_MAC); 76 | this.isPC = !!(this.isWin || this.isMac); 77 | if (this.isIOS || this.isAndroid) { 78 | var ver = ua.match(/(OS|Android) ([0-9_\.]+){1,3}/); 79 | ver = ver && ver[2].replace(/_/g, '.'); 80 | this.osVer = ver && Number(ver[0]); 81 | } else { 82 | this.osVer = null; 83 | } 84 | }; 85 | 86 | function createCommonjsModule(fn, module) { 87 | return module = { exports: {} }, fn(module, module.exports), module.exports; 88 | } 89 | 90 | var __moduleExports = createCommonjsModule(function (module) { 91 | 'use strict'; 92 | 93 | module.exports = function (str) { 94 | return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { 95 | return '%' + c.charCodeAt(0).toString(16).toUpperCase(); 96 | }); 97 | }; 98 | }); 99 | 100 | var __moduleExports$1 = createCommonjsModule(function (module) { 101 | 'use strict'; 102 | /* eslint-disable no-unused-vars */ 103 | 104 | var hasOwnProperty = Object.prototype.hasOwnProperty; 105 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 106 | 107 | function toObject(val) { 108 | if (val === null || val === undefined) { 109 | throw new TypeError('Object.assign cannot be called with null or undefined'); 110 | } 111 | 112 | return Object(val); 113 | } 114 | 115 | function shouldUseNative() { 116 | try { 117 | if (!Object.assign) { 118 | return false; 119 | } 120 | 121 | // Detect buggy property enumeration order in older V8 versions. 122 | 123 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 124 | var test1 = new String('abc'); // eslint-disable-line 125 | test1[5] = 'de'; 126 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 127 | return false; 128 | } 129 | 130 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 131 | var test2 = {}; 132 | for (var i = 0; i < 10; i++) { 133 | test2['_' + String.fromCharCode(i)] = i; 134 | } 135 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 136 | return test2[n]; 137 | }); 138 | if (order2.join('') !== '0123456789') { 139 | return false; 140 | } 141 | 142 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 143 | var test3 = {}; 144 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 145 | test3[letter] = letter; 146 | }); 147 | if (Object.keys(Object.assign({}, test3)).join('') !== 'abcdefghijklmnopqrst') { 148 | return false; 149 | } 150 | 151 | return true; 152 | } catch (e) { 153 | // We don't expect any of the above to throw, but better to be safe. 154 | return false; 155 | } 156 | } 157 | 158 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 159 | var from; 160 | var to = toObject(target); 161 | var symbols; 162 | 163 | for (var s = 1; s < arguments.length; s++) { 164 | from = Object(arguments[s]); 165 | 166 | for (var key in from) { 167 | if (hasOwnProperty.call(from, key)) { 168 | to[key] = from[key]; 169 | } 170 | } 171 | 172 | if (Object.getOwnPropertySymbols) { 173 | symbols = Object.getOwnPropertySymbols(from); 174 | for (var i = 0; i < symbols.length; i++) { 175 | if (propIsEnumerable.call(from, symbols[i])) { 176 | to[symbols[i]] = from[symbols[i]]; 177 | } 178 | } 179 | } 180 | } 181 | 182 | return to; 183 | }; 184 | }); 185 | 186 | var index = createCommonjsModule(function (module, exports) { 187 | 'use strict'; 188 | 189 | var strictUriEncode = __moduleExports; 190 | var objectAssign = __moduleExports$1; 191 | 192 | function encode(value, opts) { 193 | if (opts.encode) { 194 | return opts.strict ? strictUriEncode(value) : encodeURIComponent(value); 195 | } 196 | 197 | return value; 198 | } 199 | 200 | exports.extract = function (str) { 201 | return str.split('?')[1] || ''; 202 | }; 203 | 204 | exports.parse = function (str) { 205 | // Create an object with no prototype 206 | // https://github.com/sindresorhus/query-string/issues/47 207 | var ret = Object.create(null); 208 | 209 | if (typeof str !== 'string') { 210 | return ret; 211 | } 212 | 213 | str = str.trim().replace(/^(\?|#|&)/, ''); 214 | 215 | if (!str) { 216 | return ret; 217 | } 218 | 219 | str.split('&').forEach(function (param) { 220 | var parts = param.replace(/\+/g, ' ').split('='); 221 | // Firefox (pre 40) decodes `%3D` to `=` 222 | // https://github.com/sindresorhus/query-string/pull/37 223 | var key = parts.shift(); 224 | var val = parts.length > 0 ? parts.join('=') : undefined; 225 | 226 | key = decodeURIComponent(key); 227 | 228 | // missing `=` should be `null`: 229 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters 230 | val = val === undefined ? null : decodeURIComponent(val); 231 | 232 | if (ret[key] === undefined) { 233 | ret[key] = val; 234 | } else if (Array.isArray(ret[key])) { 235 | ret[key].push(val); 236 | } else { 237 | ret[key] = [ret[key], val]; 238 | } 239 | }); 240 | 241 | return ret; 242 | }; 243 | 244 | exports.stringify = function (obj, opts) { 245 | var defaults = { 246 | encode: true, 247 | strict: true 248 | }; 249 | 250 | opts = objectAssign(defaults, opts); 251 | 252 | return obj ? Object.keys(obj).sort().map(function (key) { 253 | var val = obj[key]; 254 | 255 | if (val === undefined) { 256 | return ''; 257 | } 258 | 259 | if (val === null) { 260 | return encode(key, opts); 261 | } 262 | 263 | if (Array.isArray(val)) { 264 | var result = []; 265 | 266 | val.slice().forEach(function (val2) { 267 | if (val2 === undefined) { 268 | return; 269 | } 270 | 271 | if (val2 === null) { 272 | result.push(encode(key, opts)); 273 | } else { 274 | result.push(encode(key, opts) + '=' + encode(val2, opts)); 275 | } 276 | }); 277 | 278 | return result.join('&'); 279 | } 280 | 281 | return encode(key, opts) + '=' + encode(val, opts); 282 | }).filter(function (x) { 283 | return x.length > 0; 284 | }).join('&') : ''; 285 | }; 286 | }); 287 | 288 | var Context = function () { 289 | function Context(context) { 290 | classCallCheck(this, Context); 291 | this._urlScheme = null; 292 | this.query = null; 293 | this.fallback = null; 294 | this.onFallback = null; 295 | this.browserback = null; 296 | this.onBrowserback = null; 297 | 298 | context && this.extend(context); 299 | } 300 | 301 | createClass(Context, [{ 302 | key: 'extend', 303 | value: function extend(target) { 304 | target.urlScheme && (this._urlScheme = target.urlScheme); 305 | target.query && (this.query = target.query); 306 | target.fallback && (this.fallback = target.fallback); 307 | target.onFallback && (this.onFallback = target.onFallback); 308 | target.browserback && (this.browserback = target.browserback); 309 | target.onBrowserback && (this.onBrowserback = target.onBrowserback); 310 | } 311 | }, { 312 | key: 'handleFallback', 313 | value: function handleFallback() { 314 | var fallback = this.fallback; 315 | 316 | if (fallback) { 317 | location.href = fallback; 318 | } 319 | this.onFallback && this.onFallback(); 320 | } 321 | }, { 322 | key: 'handleBrowserBack', 323 | value: function handleBrowserBack() { 324 | var browserback = this.browserback; 325 | 326 | if (browserback === 'back') { 327 | history.back(); 328 | } 329 | this.onBrowserback && this.onBrowserback(); 330 | } 331 | }, { 332 | key: 'urlScheme', 333 | get: function get() { 334 | var query = this.query; 335 | var _urlScheme = this._urlScheme; 336 | 337 | var fullPath = _urlScheme; 338 | if (query) { 339 | var queryStr = index.stringify(query); 340 | var parsed = new URL(fullPath); 341 | parsed.search += (parsed.search ? '&' : '?') + queryStr; 342 | fullPath = parsed.href; 343 | } 344 | 345 | return fullPath; 346 | } 347 | }]); 348 | return Context; 349 | }(); 350 | 351 | var _window = window; 352 | var document$1 = _window.document; 353 | var location$1 = _window.location; 354 | 355 | 356 | var openURLByIFrame = function openURLByIFrame(url) { 357 | var iframe = document$1.createElement('iframe'); 358 | iframe.src = url; 359 | iframe.setAttribute('height', 0); 360 | iframe.setAttribute('width', 0); 361 | iframe.style.display = 'none'; 362 | document$1.body.appendChild(iframe); 363 | return iframe; 364 | }; 365 | 366 | var openURLByHref = function openURLByHref(url, isReplace) { 367 | if (isReplace) { 368 | location$1.replace(url); 369 | } else { 370 | location$1.href = url; 371 | } 372 | }; 373 | 374 | var parseQuery = function parseQuery(str) { 375 | var queries = {}; 376 | var queryStr = str.replace(/^(\?|#|&)/, ''); 377 | var elements = queryStr.split('&'); 378 | for (var i = 0; i < elements.length; i++) { 379 | var param = elements[i]; 380 | var parts = param.replace(/\+/g, ' ').split('='); 381 | var key = parts.shift(); 382 | var val = parts.length > 0 ? parts.join('=') : undefined; 383 | key = decodeURIComponent(key); 384 | val = val === undefined ? null : decodeURIComponent(val); 385 | 386 | if (queries[key] === undefined) { 387 | queries[key] = val; 388 | } else if (Array.isArray(queries[key])) { 389 | queries[key].push(val); 390 | } else { 391 | queries[key] = [queries[key], val]; 392 | } 393 | } 394 | 395 | return queries; 396 | }; 397 | 398 | var util$1 = { 399 | openURLByIFrame: openURLByIFrame, 400 | openURLByHref: openURLByHref, 401 | parseQuery: parseQuery 402 | }; 403 | 404 | var BrowserBackgroundObserver = function () { 405 | function BrowserBackgroundObserver(onReturn, onLeave, useRequestAnimationFrame) { 406 | var _this = this; 407 | 408 | classCallCheck(this, BrowserBackgroundObserver); 409 | this.isReturnBrowser = false; 410 | this.isLeaveBrowser = false; 411 | 412 | this.onReturn = onReturn; 413 | this.onLeave = onLeave; 414 | this.useRequestAnimationFrame = useRequestAnimationFrame; 415 | 416 | this._onLeave = this._onLeave.bind(this); 417 | this._onBlur = this._onBlur.bind(this); 418 | this._onFocus = this._onFocus.bind(this); 419 | this._onVisibilityChange = this._onVisibilityChange.bind(this); 420 | this._onWebkitVisibilityChange = this._onWebkitVisibilityChange.bind(this); 421 | 422 | window.focus(); 423 | window.addEventListener('blur', this._onBlur); 424 | if (document.hidden !== undefined) { 425 | document.addEventListener('visibilitychange', this._onVisibilityChange); 426 | } else if (document.webkitHidden !== undefined) { 427 | document.addEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 428 | } 429 | 430 | if (useRequestAnimationFrame) { 431 | (function () { 432 | var observe = function observe() { 433 | _this.rafId = window.requestAnimationFrame(observe); 434 | if (_this.rafTimer !== false) { 435 | clearTimeout(_this.rafTimer); 436 | } 437 | 438 | _this.rafTimer = setTimeout(function () { 439 | if (!isReturnBrowser) { 440 | _onReturn(); 441 | } 442 | 443 | window.cancelAnimationFrame(_this.rafId); 444 | }, 1000); 445 | }; 446 | 447 | observe(); 448 | })(); 449 | } 450 | } 451 | 452 | createClass(BrowserBackgroundObserver, [{ 453 | key: '_onReturn', 454 | value: function _onReturn() { 455 | var rafTimer = this.rafTimer; 456 | var rafId = this.rafId; 457 | 458 | this.isReturnBrowser = true; 459 | document.removeEventListener('visibilitychange', this._onVisibilityChange); 460 | document.removeEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 461 | window.removeEventListener('focus', this._onFocus); 462 | clearTimeout(rafTimer); 463 | window.cancelAnimationFrame && window.cancelAnimationFrame(rafId); 464 | 465 | this.onReturn && this.onReturn(); 466 | } 467 | }, { 468 | key: '_onLeave', 469 | value: function _onLeave() { 470 | this.isLeaveBrowser = true; 471 | this.onLeave && this.onLeave(); 472 | } 473 | }, { 474 | key: '_onBlur', 475 | value: function _onBlur() { 476 | var isLeaveBrowser = this.isLeaveBrowser; 477 | 478 | window.removeEventListener('blur', this._onBlur); 479 | window.addEventListener('focus', this._onFocus); 480 | if (!isLeaveBrowser) { 481 | this._onLeave(); 482 | } 483 | } 484 | }, { 485 | key: '_onFocus', 486 | value: function _onFocus() { 487 | var isReturnBrowser = this.isReturnBrowser; 488 | 489 | if (!isReturnBrowser) { 490 | this._onReturn(); 491 | } 492 | } 493 | }, { 494 | key: '_onVisibilityChange', 495 | value: function _onVisibilityChange() { 496 | var isLeaveBrowser = this.isLeaveBrowser; 497 | var isReturnBrowser = this.isReturnBrowser; 498 | 499 | if (!isLeaveBrowser && document.hidden) { 500 | this._onLeave(); 501 | } else if (!isReturnBrowser && !document.hidden) { 502 | this._onReturn(); 503 | } 504 | } 505 | }, { 506 | key: '_onWebkitVisibilityChange', 507 | value: function _onWebkitVisibilityChange() { 508 | var isLeaveBrowser = this.isLeaveBrowser; 509 | var isReturnBrowser = this.isReturnBrowser; 510 | 511 | if (!isLeaveBrowser && document.webkitHidden) { 512 | this._onLeave(); 513 | } else if (!isReturnBrowser && !document.webkitHidden) { 514 | this._onReturn(); 515 | } 516 | } 517 | }]); 518 | return BrowserBackgroundObserver; 519 | }(); 520 | 521 | var BaseLauncher = function () { 522 | function BaseLauncher(context) { 523 | classCallCheck(this, BaseLauncher); 524 | this._context = null; 525 | this._fallbackTime = 500; 526 | 527 | this._context = context; 528 | 529 | this._handleTimeout = this._handleTimeout.bind(this); 530 | this._handleLeaveBrowser = this._handleLeaveBrowser.bind(this); 531 | this._handleBrowserBack = this._handleBrowserBack.bind(this); 532 | } 533 | 534 | createClass(BaseLauncher, [{ 535 | key: 'launch', 536 | value: function launch() { 537 | util$1.openURLByIFrame(this._context.urlScheme); 538 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 539 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 540 | } 541 | }, { 542 | key: '_handleTimeout', 543 | value: function _handleTimeout() { 544 | this._context.handleFallback(); 545 | } 546 | }, { 547 | key: '_handleLeaveBrowser', 548 | value: function _handleLeaveBrowser() { 549 | var _timerId = this._timerId; 550 | 551 | clearTimeout(_timerId); 552 | } 553 | }, { 554 | key: '_handleBrowserBack', 555 | value: function _handleBrowserBack() { 556 | this._context.handleBrowserBack(); 557 | } 558 | }]); 559 | return BaseLauncher; 560 | }(); 561 | 562 | var IOS = function (_BaseLauncher) { 563 | inherits(IOS, _BaseLauncher); 564 | 565 | function IOS() { 566 | classCallCheck(this, IOS); 567 | return possibleConstructorReturn(this, (IOS.__proto__ || Object.getPrototypeOf(IOS)).apply(this, arguments)); 568 | } 569 | 570 | createClass(IOS, [{ 571 | key: 'launch', 572 | value: function launch() { 573 | var _context = this._context; 574 | 575 | if (_context.osVer > 8) { 576 | util$1.openURLByHref(_context.urlScheme); 577 | } else { 578 | util$1.openURLByIFrame(_context.urlScheme); 579 | } 580 | 581 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 582 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 583 | } 584 | }]); 585 | return IOS; 586 | }(BaseLauncher); 587 | 588 | var Android = function (_BaseLauncher) { 589 | inherits(Android, _BaseLauncher); 590 | 591 | function Android() { 592 | classCallCheck(this, Android); 593 | return possibleConstructorReturn(this, (Android.__proto__ || Object.getPrototypeOf(Android)).apply(this, arguments)); 594 | } 595 | 596 | return Android; 597 | }(BaseLauncher); 598 | 599 | var PC = function (_BaseLauncher) { 600 | inherits(PC, _BaseLauncher); 601 | 602 | function PC(context) { 603 | classCallCheck(this, PC); 604 | 605 | var _this = possibleConstructorReturn(this, (PC.__proto__ || Object.getPrototypeOf(PC)).call(this, context)); 606 | 607 | _this._enableEventListener(window); 608 | _this._enableEventListener(document); 609 | return _this; 610 | } 611 | 612 | createClass(PC, [{ 613 | key: 'launch', 614 | value: function launch() { 615 | util$1.openURLByIFrame(this._context.urlScheme); 616 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 617 | this._observer = new BrowserBackgroundObserver(this._handleBrowserBack, this._handleLeaveBrowser, true); 618 | } 619 | }, { 620 | key: '_enableEventListener', 621 | value: function _enableEventListener(target) { 622 | if (!target.addEventListener) { 623 | target.addEventListener = function (type, callback) { 624 | target.attachEvent('on' + type, callback); 625 | }; 626 | } 627 | 628 | if (!target.removeEventListener) { 629 | target.removeEventListener = function (type, callback) { 630 | target.detachEvent('on' + type, callback); 631 | }; 632 | } 633 | } 634 | }]); 635 | return PC; 636 | }(BaseLauncher); 637 | 638 | var util = { 639 | IOS: IOS, 640 | Android: Android, 641 | PC: PC 642 | }; 643 | 644 | var FallbackCustomScheme = function () { 645 | function FallbackCustomScheme(options) { 646 | classCallCheck(this, FallbackCustomScheme); 647 | this._ua = new UserAgent(); 648 | 649 | this._context = new Context(options); 650 | } 651 | 652 | createClass(FallbackCustomScheme, [{ 653 | key: 'launch', 654 | value: function launch(options) { 655 | var _context = this._context; 656 | var _ua = this._ua; 657 | 658 | options && _context.extend(options); 659 | var urlScheme = _context.urlScheme; 660 | var isIOS = _ua.isIOS; 661 | var isAndroid = _ua.isAndroid; 662 | var isPC = _ua.isPC; 663 | 664 | 665 | if (!urlScheme) { 666 | throw new Error('you must set "urlScheme"'); 667 | } 668 | 669 | if (isIOS) { 670 | this._launcher = new IOS(_context); 671 | } else if (isAndroid) { 672 | this._launcher = new Android(_context); 673 | } else if (isPC) { 674 | this._launcher = new PC(_context); 675 | } 676 | if (this._launcher) { 677 | this._launcher.launch(); 678 | } 679 | } 680 | }]); 681 | return FallbackCustomScheme; 682 | }(); 683 | 684 | return FallbackCustomScheme; 685 | 686 | }))); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 |

FallbackCustomScheme demo

29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 | 43 |
44 |
45 | 46 |
47 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fallback-custom-scheme", 3 | "version": "0.1.4", 4 | "description": "Launcher application (iOS, Android, Windows, Mac) from web page.", 5 | "main": "bundle.js", 6 | "scripts": { 7 | "build": "rollup -c rollup.config.js && cp ./bundle.js ./docs/bundle.js", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "watch": "onchange 'src/**/*.js' -- npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SatoshiKawabata/fallback-custom-scheme" 14 | }, 15 | "author": "SatoshiKawabata (http://qiita.com/kwst)", 16 | "license": "ISC", 17 | "dependencies": { 18 | "jquery": "^1.12.4", 19 | "query-string": "^4.2.3" 20 | }, 21 | "devDependencies": { 22 | "babel-eslint": "^6.1.2", 23 | "babel-preset-es2015-rollup": "^1.2.0", 24 | "babel-preset-stage-2": "^6.13.0", 25 | "eslint": "^3.4.0", 26 | "eslint-config-airbnb": "^10.0.1", 27 | "eslint-plugin-import": "^1.14.0", 28 | "eslint-plugin-jsx-a11y": "^2.2.1", 29 | "eslint-plugin-react": "^6.2.0", 30 | "onchange": "^3.0.0", 31 | "rollup": "^0.34.13", 32 | "rollup-plugin-babel": "^2.6.1", 33 | "rollup-plugin-commonjs": "^4.1.0", 34 | "rollup-plugin-json": "^2.0.1", 35 | "rollup-plugin-node-resolve": "^2.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import npm from 'rollup-plugin-node-resolve'; 4 | 5 | export default { 6 | entry: 'src/index.js', 7 | dest: 'bundle.js', 8 | format: 'umd', 9 | moduleName: 'FallbackCustomScheme', 10 | plugins: [ 11 | npm({ jsnext: true }), 12 | commonjs(), 13 | babel(), 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/Context.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string'; 2 | 3 | class Context { 4 | 5 | _urlScheme = null; 6 | query = null; 7 | fallback = null; 8 | onFallback = null; 9 | browserback = null; 10 | onBrowserback = null; 11 | 12 | constructor(context) { 13 | context && this.extend(context); 14 | } 15 | 16 | get urlScheme() { 17 | const { query, _urlScheme } = this; 18 | let fullPath = _urlScheme; 19 | if (query) { 20 | const queryStr = queryString.stringify(query); 21 | const parsed = new URL(fullPath); 22 | parsed.search += (parsed.search ? '&' : '?') + queryStr; 23 | fullPath = parsed.href; 24 | } 25 | 26 | return fullPath; 27 | } 28 | 29 | extend(target) { 30 | target.urlScheme && (this._urlScheme = target.urlScheme); 31 | target.query && (this.query = target.query); 32 | target.fallback && (this.fallback = target.fallback); 33 | target.onFallback && (this.onFallback = target.onFallback); 34 | target.browserback && (this.browserback = target.browserback); 35 | target.onBrowserback && (this.onBrowserback = target.onBrowserback); 36 | } 37 | 38 | handleFallback() { 39 | const { fallback } = this; 40 | if (fallback) { 41 | location.href = fallback; 42 | } 43 | this.onFallback && this.onFallback(); 44 | } 45 | 46 | handleBrowserBack() { 47 | const { browserback } = this; 48 | if (browserback === 'back') { 49 | history.back(); 50 | } 51 | this.onBrowserback && this.onBrowserback(); 52 | } 53 | } 54 | 55 | export default Context; 56 | -------------------------------------------------------------------------------- /src/UserAgent.js: -------------------------------------------------------------------------------- 1 | const RE_IS_IPHONE = /(iPhone\sOS)\s([\d_]+)/; 2 | const RE_IS_IPAD = /(iPad).*OS\s([\d_]+)/; 3 | const RE_IS_IPOD_TOUCH = /(iPod touch).*OS\s([\d_]+)/; 4 | const RE_IS_ANDROID = /(Android)\s+([\d.]+)/; 5 | const RE_IS_WIN = /Windows/; 6 | const RE_IS_MAC = /Mac OS/; 7 | 8 | class UserAgent { 9 | isIOS = null; 10 | isAndroid = null; 11 | isWin = null; 12 | isMac = null; 13 | isPC = null; 14 | osVer = null; 15 | 16 | constructor() { 17 | const ua = navigator.userAgent; 18 | this.isIOS = !!ua.match(RE_IS_IPHONE) || ua.match(RE_IS_IPAD) || ua.match(RE_IS_IPOD_TOUCH); 19 | this.isAndroid = !!ua.match(RE_IS_ANDROID); 20 | this.isWin = !!ua.match(RE_IS_WIN); 21 | this.isMac = !!ua.match(RE_IS_MAC); 22 | this.isPC = !!(this.isWin || this.isMac); 23 | if (this.isIOS || this.isAndroid) { 24 | let ver = ua.match(/(OS|Android) ([0-9_\.]+){1,3}/); 25 | ver = ver && ver[2].replace(/_/g, '.'); 26 | this.osVer = ver && Number(ver[0]); 27 | } else { 28 | this.osVer = null; 29 | } 30 | } 31 | } 32 | 33 | export default UserAgent; 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import UserAgent from './UserAgent'; 2 | import Context from './Context'; 3 | import { 4 | IOS, 5 | Android, 6 | PC, 7 | } from './launcher/'; 8 | 9 | class FallbackCustomScheme { 10 | _context; 11 | _ua = new UserAgent(); 12 | _launcher; 13 | 14 | constructor(options) { 15 | this._context = new Context(options); 16 | } 17 | 18 | launch(options) { 19 | const { 20 | _context, 21 | _ua, 22 | } = this; 23 | options && _context.extend(options); 24 | const { 25 | urlScheme, 26 | } = _context; 27 | const { 28 | isIOS, 29 | isAndroid, 30 | isPC, 31 | } = _ua; 32 | 33 | if (!urlScheme) { 34 | throw new Error('you must set "urlScheme"'); 35 | } 36 | 37 | if (isIOS) { 38 | this._launcher = new IOS(_context); 39 | } else if (isAndroid) { 40 | this._launcher = new Android(_context); 41 | } else if (isPC) { 42 | this._launcher = new PC(_context); 43 | } 44 | if (this._launcher) { 45 | this._launcher.launch(); 46 | } 47 | } 48 | } 49 | 50 | export default FallbackCustomScheme; 51 | -------------------------------------------------------------------------------- /src/launcher/Android.js: -------------------------------------------------------------------------------- 1 | import BaseLauncher from './Base'; 2 | 3 | class Android extends BaseLauncher { 4 | } 5 | export default Android; 6 | -------------------------------------------------------------------------------- /src/launcher/Base.js: -------------------------------------------------------------------------------- 1 | import util from '../util'; 2 | import BrowserBackgroundObserver from './BrowserBackgroundObserver'; 3 | 4 | class BaseLauncher { 5 | _context = null; 6 | _fallbackTime = 500; 7 | _timerId; 8 | _observer; 9 | 10 | constructor(context) { 11 | this._context = context; 12 | 13 | this._handleTimeout = this._handleTimeout.bind(this); 14 | this._handleLeaveBrowser = this._handleLeaveBrowser.bind(this); 15 | this._handleBrowserBack = this._handleBrowserBack.bind(this); 16 | } 17 | 18 | launch() { 19 | util.openURLByIFrame(this._context.urlScheme); 20 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 21 | this._observer = new BrowserBackgroundObserver( 22 | this._handleBrowserBack, 23 | this._handleLeaveBrowser, 24 | true); 25 | } 26 | 27 | _handleTimeout() { 28 | this._context.handleFallback(); 29 | } 30 | 31 | _handleLeaveBrowser() { 32 | const { _timerId } = this; 33 | clearTimeout(_timerId); 34 | } 35 | 36 | _handleBrowserBack() { 37 | this._context.handleBrowserBack(); 38 | } 39 | } 40 | 41 | export default BaseLauncher; 42 | -------------------------------------------------------------------------------- /src/launcher/BrowserBackgroundObserver.js: -------------------------------------------------------------------------------- 1 | class BrowserBackgroundObserver { 2 | onReturn; 3 | onLeave; 4 | useRequestAnimationFrame; 5 | isReturnBrowser = false; 6 | isLeaveBrowser = false; 7 | rafId; 8 | rafTimer; 9 | 10 | constructor(onReturn, onLeave, useRequestAnimationFrame) { 11 | this.onReturn = onReturn; 12 | this.onLeave = onLeave; 13 | this.useRequestAnimationFrame = useRequestAnimationFrame; 14 | 15 | this._onLeave = this._onLeave.bind(this); 16 | this._onBlur = this._onBlur.bind(this); 17 | this._onFocus = this._onFocus.bind(this); 18 | this._onVisibilityChange = this._onVisibilityChange.bind(this); 19 | this._onWebkitVisibilityChange = this._onWebkitVisibilityChange.bind(this); 20 | 21 | window.focus(); 22 | window.addEventListener('blur', this._onBlur); 23 | if (document.hidden !== undefined) { 24 | document.addEventListener('visibilitychange', this._onVisibilityChange); 25 | } else if (document.webkitHidden !== undefined) { 26 | document.addEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 27 | } 28 | 29 | if (useRequestAnimationFrame) { 30 | const observe = () => { 31 | this.rafId = window.requestAnimationFrame(observe); 32 | if (this.rafTimer !== false) { 33 | clearTimeout(this.rafTimer); 34 | } 35 | 36 | this.rafTimer = setTimeout(() => { 37 | if (!isReturnBrowser) { 38 | _onReturn(); 39 | } 40 | 41 | window.cancelAnimationFrame(this.rafId); 42 | }, 1000); 43 | }; 44 | 45 | observe(); 46 | } 47 | } 48 | 49 | _onReturn() { 50 | const { 51 | rafTimer, 52 | rafId, 53 | } = this; 54 | this.isReturnBrowser = true; 55 | document.removeEventListener('visibilitychange', this._onVisibilityChange); 56 | document.removeEventListener('webkitvisibilitychange', this._onWebkitVisibilityChange); 57 | window.removeEventListener('focus', this._onFocus); 58 | clearTimeout(rafTimer); 59 | window.cancelAnimationFrame && window.cancelAnimationFrame(rafId); 60 | 61 | this.onReturn && this.onReturn(); 62 | } 63 | 64 | _onLeave() { 65 | this.isLeaveBrowser = true; 66 | this.onLeave && this.onLeave(); 67 | } 68 | 69 | _onBlur() { 70 | const { 71 | isLeaveBrowser, 72 | } = this; 73 | window.removeEventListener('blur', this._onBlur); 74 | window.addEventListener('focus', this._onFocus); 75 | if (!isLeaveBrowser) { 76 | this._onLeave(); 77 | } 78 | } 79 | 80 | _onFocus() { 81 | const { 82 | isReturnBrowser, 83 | } = this; 84 | if (!isReturnBrowser) { 85 | this._onReturn(); 86 | } 87 | } 88 | 89 | _onVisibilityChange() { 90 | const { 91 | isLeaveBrowser, 92 | isReturnBrowser, 93 | } = this; 94 | if (!isLeaveBrowser && document.hidden) { 95 | this._onLeave(); 96 | } else if (!isReturnBrowser && !document.hidden) { 97 | this._onReturn(); 98 | } 99 | } 100 | 101 | _onWebkitVisibilityChange() { 102 | const { 103 | isLeaveBrowser, 104 | isReturnBrowser, 105 | } = this; 106 | if (!isLeaveBrowser && document.webkitHidden) { 107 | this._onLeave(); 108 | } else if (!isReturnBrowser && !document.webkitHidden) { 109 | this._onReturn(); 110 | } 111 | } 112 | } 113 | 114 | export default BrowserBackgroundObserver; 115 | -------------------------------------------------------------------------------- /src/launcher/IOS.js: -------------------------------------------------------------------------------- 1 | import util from '../util'; 2 | import BaseLauncher from './Base'; 3 | import BrowserBackgroundObserver from './BrowserBackgroundObserver'; 4 | 5 | class IOS extends BaseLauncher { 6 | launch() { 7 | const { _context } = this; 8 | if (_context.osVer > 8) { 9 | util.openURLByHref(_context.urlScheme); 10 | } else { 11 | util.openURLByIFrame(_context.urlScheme); 12 | } 13 | 14 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 15 | this._observer = new BrowserBackgroundObserver( 16 | this._handleBrowserBack, 17 | this._handleLeaveBrowser, 18 | true); 19 | } 20 | } 21 | export default IOS; 22 | -------------------------------------------------------------------------------- /src/launcher/PC.js: -------------------------------------------------------------------------------- 1 | import util from '../util'; 2 | import BaseLauncher from './Base'; 3 | import BrowserBackgroundObserver from './BrowserBackgroundObserver'; 4 | 5 | class PC extends BaseLauncher { 6 | 7 | constructor(context) { 8 | super(context); 9 | this._enableEventListener(window); 10 | this._enableEventListener(document); 11 | } 12 | 13 | launch() { 14 | util.openURLByIFrame(this._context.urlScheme); 15 | this._timerId = setTimeout(this._handleTimeout, this._fallbackTime); 16 | this._observer = new BrowserBackgroundObserver( 17 | this._handleBrowserBack, 18 | this._handleLeaveBrowser, 19 | true); 20 | } 21 | 22 | _enableEventListener(target) { 23 | if (!target.addEventListener) { 24 | target.addEventListener = (type, callback) => { 25 | target.attachEvent(`on${type}`, callback); 26 | }; 27 | } 28 | 29 | if (!target.removeEventListener) { 30 | target.removeEventListener = (type, callback) => { 31 | target.detachEvent(`on${type}`, callback); 32 | }; 33 | } 34 | } 35 | } 36 | export default PC; 37 | -------------------------------------------------------------------------------- /src/launcher/index.js: -------------------------------------------------------------------------------- 1 | import IOS from './IOS'; 2 | import Android from './Android'; 3 | import PC from './PC'; 4 | 5 | const util = { 6 | IOS, 7 | Android, 8 | PC, 9 | }; 10 | export default util; 11 | export { 12 | IOS, 13 | Android, 14 | PC, 15 | }; 16 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const { document, location } = window; 2 | 3 | const openURLByIFrame = (url) => { 4 | const iframe = document.createElement('iframe'); 5 | iframe.src = url; 6 | iframe.setAttribute('height', 0); 7 | iframe.setAttribute('width', 0); 8 | iframe.style.display = 'none'; 9 | document.body.appendChild(iframe); 10 | return iframe; 11 | }; 12 | 13 | const openURLByHref = (url, isReplace) => { 14 | if (isReplace) { 15 | location.replace(url); 16 | } else { 17 | location.href = url; 18 | } 19 | }; 20 | 21 | const parseQuery = (str) => { 22 | const queries = {}; 23 | const queryStr = str.replace(/^(\?|#|&)/, ''); 24 | const elements = queryStr.split('&'); 25 | for (let i = 0; i < elements.length; i++) { 26 | const param = elements[i]; 27 | const parts = param.replace(/\+/g, ' ').split('='); 28 | let key = parts.shift(); 29 | let val = parts.length > 0 ? parts.join('=') : undefined; 30 | key = decodeURIComponent(key); 31 | val = val === undefined ? null : decodeURIComponent(val); 32 | 33 | if (queries[key] === undefined) { 34 | queries[key] = val; 35 | } else if (Array.isArray(queries[key])) { 36 | queries[key].push(val); 37 | } else { 38 | queries[key] = [ 39 | queries[key], 40 | val, 41 | ]; 42 | } 43 | } 44 | 45 | return queries; 46 | }; 47 | 48 | export default { 49 | openURLByIFrame, 50 | openURLByHref, 51 | parseQuery, 52 | }; 53 | --------------------------------------------------------------------------------