├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── stale.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── docs ├── COMMIT_MESSAGE_CONVENTION.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── getting-started.md ├── issue-chrome.md ├── issue-iOS9.md ├── issue-iframe.md ├── issue-remain-page.md ├── issue-webview.md └── v2.0.0-migration-guide.md ├── examples ├── css │ └── tui-example-style.css ├── example01-basic.html └── example02-using-iframe.html ├── package-lock.json ├── package.json ├── src └── js │ ├── appLoader.js │ ├── detectors.js │ ├── etcDetectors.js │ ├── iosDetectors.js │ └── util.js ├── tui-note.config.js ├── tuidoc.config.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | tui-note.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['tui', 'plugin:prettier/recommended'], 3 | env: { 4 | browser: true, 5 | amd: true, 6 | node: true, 7 | commonjs: true 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Bug 6 | assignees: '' 7 | --- 8 | 9 | ## Describe the bug 10 | A clear and concise description of what the bug is. 11 | 12 | ## To Reproduce 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | ## Expected behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Screenshots 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | ## Desktop (please complete the following information): 26 | - OS: [e.g. iOS] 27 | - Browser: [e.g. chrome, safari] 28 | - Version: [e.g. 22] 29 | 30 | ## Smartphone (please complete the following information): 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser: [e.g. stock browser, safari] 34 | - Version: [e.g. 22] 35 | 36 | ## Additional context 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Enhancement, Need Discussion 6 | assignees: '' 7 | --- 8 | 9 | 15 | 16 | ## Version 17 | Write the version that you are currently using. 18 | 19 | ## Development Environment 20 | Write the browser type, OS and so on. 21 | 22 | ## Current Behavior 23 | Write a description of the current operation. You can add sample code, 'CodePen' or 'jsfiddle' links. 24 | 25 | ```js 26 | // Write example code 27 | ``` 28 | 29 | ## Expected Behavior 30 | Write a description of the future action. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Create a question about the Tree 4 | title: '' 5 | labels: Question 6 | assignees: '' 7 | --- 8 | 9 | 18 | 19 | ## Summary 20 | 21 | A clear and concise description of what the question is. 22 | 23 | ## Screenshots 24 | 25 | If applicable, add screenshots to help explain your question. 26 | 27 | ## Version 28 | 29 | Write the version of the Editor you are currently using. 30 | 31 | ## Additional context 32 | 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | 2 | # Configuration for probot-stale - https://github.com/probot/stale 3 | 4 | # Number of days of inactivity before an Issue or Pull Request becomes stale 5 | daysUntilStale: 30 6 | 7 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 8 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 9 | daysUntilClose: 7 10 | 11 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 12 | exemptLabels: 13 | - Type: Feature 14 | - Type: Enhancement 15 | - Type: Bug 16 | - NHN Cloud 17 | 18 | # Label to use when marking as stale 19 | staleLabel: inactive 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as inactive because there hasn’t been much going on it lately. 24 | It is going to be closed after 7 days. Thanks! 25 | # Comment to post when closing a stale Issue or Pull Request. 26 | closeComment: > -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Compiled files 11 | dist 12 | 13 | # Dependency directory 14 | node_modules 15 | 16 | # Bower Components 17 | bower_components 18 | 19 | #JSDOC 20 | doc 21 | 22 | # IDEA 23 | .idea 24 | *.iml 25 | 26 | # Window 27 | Thumbs.db 28 | Desktop.ini 29 | 30 | # MACgi 31 | .DS_Store 32 | 33 | # eclipse 34 | .project 35 | .metadata 36 | 37 | #report / screenshots 38 | report 39 | screenshots 40 | 41 | # Atom 42 | tags 43 | .ctags 44 | .tern-project 45 | 46 | # etc 47 | .agignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth" : 100, 3 | "singleQuote" : true 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dl_javascript@nhn.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TOAST UI 2 | 3 | First off, thanks for taking the time to contribute! 🎉 😘 ✨ 4 | 5 | The following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Reporting Bugs 8 | Bugs are tracked as GitHub issues. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template. 9 | 10 | Explain the problem and include additional details to help maintainers reproduce the problem: 11 | 12 | * **Use a clear and descriptive title** for the issue to identify the problem. 13 | * **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard. 14 | * **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks. 15 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 16 | * **Explain which behavior you expected to see instead and why.** 17 | * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. 18 | 19 | ## Suggesting Enhancements 20 | In case you want to suggest for a TOAST UI product, please follow this guideline to help maintainers and the community understand your suggestion. 21 | Before creating suggestions, please check [issue list](../../../labels/feature%20request) if there's already a request. 22 | 23 | Create an issue and provide the following information: 24 | 25 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 26 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 27 | * **Provide specific examples to demonstrate the steps.** Include copy/pasteable snippets which you use in those examples, as Markdown code blocks. 28 | * **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of a TOAST UI product which the suggestion is related to. 29 | * **Explain why this enhancement would be useful** to most TOAST UI users. 30 | * **List some other products where this enhancement exists.** 31 | 32 | ## First Code Contribution 33 | 34 | Unsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues: 35 | 36 | * **document issues**: issues which should be reviewed or improved. 37 | * **good first issues**: issues which should only require a few lines of code, and a test or two. 38 | * **help wanted issues**: issues which should be a bit more involved than beginner issues. 39 | 40 | ## Pull Requests 41 | 42 | ### Development WorkFlow 43 | - Set up your development environment 44 | - Make change from a right branch 45 | - Be sure the code passes `npm run test` 46 | - Make a pull request 47 | 48 | ### Development environment 49 | - Prepare your machine node and it's packages installed. 50 | - Checkout our repository 51 | - Install dependencies by `npm install && bower install` 52 | - Start webpack-dev-server by `npm run serve` 53 | 54 | ### Make changes 55 | #### Checkout a branch 56 | - **develop**: PR base branch. merge features, updates for next minor or major release 57 | - **master**: bug fix or document update for next patch release. develop branch will rebase every time master branch update. so keep code change to a minimum. 58 | - **production**: lastest release branch with distribution files. never make a PR on this 59 | - **gh-pages**: API docs, examples and demo 60 | 61 | #### Check Code Style 62 | Run `npm run eslint` and make sure all the tests pass. 63 | 64 | #### Test 65 | Run `npm run test` and verify all the tests pass. 66 | If you are adding new commands or features, they must include tests. 67 | If you are changing functionality, update the tests if you need to. 68 | 69 | #### Commit 70 | Follow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md). 71 | 72 | ### Yes! Pull request 73 | Make your pull request, then describe your changes. 74 | #### Title 75 | Follow other PR title format on below. 76 | ``` 77 | : Short Description (fix #111) 78 | : Short Description (fix #123, #111, #122) 79 | : Short Description (ref #111) 80 | ``` 81 | * capitalize first letter of Type 82 | * use present tense: 'change' not 'changed' or 'changes' 83 | 84 | #### Description 85 | If it has related to issues, add links to the issues(like `#123`) in the description. 86 | Fill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case. 87 | 88 | ## Code of Conduct 89 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@nhn.com. 90 | 91 | > This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests) 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 NHN Cloud Corp. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOAST UI Component : App Loader 2 | > Component that installs a specific app by determining whether an app is installed on mobile devices. 3 | 4 | [![GitHub release](https://img.shields.io/github/release/nhn/tui.app-loader.svg)](https://github.com/nhn/tui.app-loader/releases/latest) 5 | [![npm](https://img.shields.io/npm/v/tui-app-loader.svg)](https://www.npmjs.com/package/tui-app-loader) 6 | [![GitHub license](https://img.shields.io/github/license/nhn/tui.app-loader.svg)](https://github.com/nhn/tui.app-loader/blob/production/LICENSE) 7 | [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.project-name/labels/help%20wanted) 8 | [![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn) 9 | 10 | 11 | ## 🚩 Table of Contents 12 | 13 | - [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source) 14 | - [📙 Documents](#-documents) 15 | - [🎨 Features](#-features) 16 | - [🐾 Examples](#-examples) 17 | - [💾 Install](#-install) 18 | - [Via Package Manager](#via-package-manager) 19 | - [npm](#npm) 20 | - [bower](#bower) 21 | - [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn) 22 | - [Download Source Files](#download-source-files) 23 | - [🔨 Usage](#-usage) 24 | - [HTML](#html) 25 | - [JavaScript](#javascript) 26 | - [Using namespace in browser environment](#using-namespace-in-browser-environment) 27 | - [Using module format in node environment](#using-module-format-in-node-environment) 28 | - [🔧 Pull Request Steps](#-pull-request-steps) 29 | - [Setup](#setup) 30 | - [Develop](#develop) 31 | - [Run webpack-dev-server](#run-webpack-dev-server) 32 | - [Run karma test](#run-karma-test) 33 | - [Pull Request](#pull-request) 34 | - [🔩 Dependency](#-dependency) 35 | - [💬 Contributing](#-contributing) 36 | - [🍞 TOAST UI Family](#-toast-ui-family) 37 | - [📜 License](#-license) 38 | 39 | 40 | ## Collect statistics on the use of open source 41 | 42 | TOAST UI AppLoader applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI AppLoader is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the following `usageStatistics` options when creating the instance. 43 | 44 | ```js 45 | const options = { 46 | ... 47 | usageStatistics: false 48 | } 49 | const instance = new AppLoader(options); 50 | ``` 51 | 52 | Or, include `tui-code-snippet.js` (**v2.2.0** or **later**) and then immediately write the options as follows: 53 | 54 | ```js 55 | tui.usageStatistics = false; 56 | ``` 57 | 58 | 59 | ## 📙 Documents 60 | * [Getting Started](https://github.com/nhn/tui.app-loader/blob/production/docs/getting-started.md) 61 | * [Tutorials](https://github.com/nhn/tui.app-loader/tree/production/docs) 62 | * [APIs](https://nhn.github.io/tui.app-loader/latest) 63 | 64 | You can also see the older versions of API page on the [releases page](https://github.com/nhn/tui.app-loader/releases). 65 | 66 | 67 | ## 🎨 Features 68 | * Supports various options of url schemes for app calls. 69 | * Supports `Universal link` that does not need to set fallback url for iOS9+. 70 | * Supports `intentURI` for Chromium 25+. 71 | * Sets the time to delay app calls. 72 | 73 | 74 | ## 🐾 Examples 75 | * [Basic](https://nhn.github.io/tui.app-loader/latest/tutorial-example01-basic) : Example using default options. 76 | 77 | More examples can be found on the left sidebar of each example page, and have fun with it. 78 | 79 | 80 | ## 💾 Install 81 | 82 | TOAST UI products can be used by using the package manager or downloading the source directly. 83 | However, we highly recommend using the package manager. 84 | 85 | ### Via Package Manager 86 | 87 | TOAST UI products are registered in two package managers, [npm](https://www.npmjs.com/) and [bower](https://bower.io/). 88 | You can conveniently install it using the commands provided by each package manager. 89 | When using npm, be sure to use it in the environment [Node.js](https://nodejs.org/ko/) is installed. 90 | 91 | #### npm 92 | 93 | ``` sh 94 | $ npm install --save tui-app-loader # Latest version 95 | $ npm install --save tui-app-loader@ # Specific version 96 | ``` 97 | 98 | #### bower 99 | 100 | ``` sh 101 | $ bower install tui-app-loader # Latest version 102 | $ bower install tui-app-loader# # Specific version 103 | ``` 104 | 105 | ### Via Contents Delivery Network (CDN) 106 | TOAST UI products are available over the CDN powered by [TOAST Cloud](https://www.toast.com). 107 | 108 | You can use the CDN as below. 109 | 110 | ```html 111 | 112 | ``` 113 | 114 | If you want to use a specific version, use the tag name instead of `latest` in the url's path. 115 | 116 | The CDN directory has the following structure. 117 | 118 | ``` 119 | tui-app-loader/ 120 | ├─ latest/ 121 | │ ├─ tui-app-loader.js 122 | │ └─ tui-app-loader.min.js 123 | ├─ v2.1.0/ 124 | │ ├─ ... 125 | ``` 126 | 127 | ### Download Source Files 128 | * [Download bundle files](https://github.com/nhn/tui.app-loader/tree/production/dist) 129 | * [Download all sources for each version](https://github.com/nhn/tui.app-loader/releases) 130 | 131 | 132 | ## 🔨 Usage 133 | 134 | ### HTML 135 | 136 | This component does not require the container element. 137 | 138 | ### JavaScript 139 | 140 | This component can be used by creating an instance with the constructor function. 141 | To get the constructor function, you should import the module using one of the following ways depending on your environment. 142 | 143 | #### Using namespace in browser environment 144 | ``` javascript 145 | const AppLoader = tui.AppLoader; 146 | ``` 147 | 148 | #### Using module format in node environment 149 | ``` javascript 150 | const AppLoader = require('tui-app-loader'); /* CommonJS */ 151 | ``` 152 | 153 | ``` javascript 154 | import AppLoader from 'tui-app-loader'; /* ES6 */ 155 | ``` 156 | 157 | You should call `exec` method with [options](http://nhn.github.io/tui.app-loader/latest/AppLoader#exec) after creating an instance. 158 | 159 | ``` javascript 160 | const appLoader = new AppLoader(); 161 | appLoader.exec({ ... }); 162 | ``` 163 | 164 | 165 | ## 🔧 Pull Request Steps 166 | 167 | TOAST UI products are open source, so you can create a pull request(PR) after you fix issues. 168 | Run npm scripts and develop yourself with the following process. 169 | 170 | ### Setup 171 | 172 | Fork `develop` branch into your personal repository. 173 | Clone it to local computer. Install node modules. 174 | Before starting development, you should check if there are any errors. 175 | 176 | ``` sh 177 | $ git clone https://github.com/{your-personal-repo}/tui.app-loader.git 178 | $ cd tui.app-loader 179 | $ npm install 180 | $ npm run test 181 | ``` 182 | 183 | ### Develop 184 | 185 | Let's start development! 186 | You can see your code reflected as soon as you save the code by running a server. 187 | Don't miss adding test cases and then make green rights. 188 | 189 | #### Run webpack-dev-server 190 | 191 | ``` sh 192 | $ npm run serve 193 | $ npm run serve:ie8 # Run on Internet Explorer 8 194 | ``` 195 | 196 | #### Run karma test 197 | 198 | ``` sh 199 | $ npm run test 200 | ``` 201 | 202 | ### Pull Request 203 | 204 | Before uploading your PR, run test one last time to check if there are any errors.. 205 | If it has no errors, commit and then push it! 206 | 207 | For more information on PR's steps, please see links in the Contributing section. 208 | 209 | 210 | ## 🔩 Dependency 211 | * [ua-parser-js](https://github.com/faisalman/ua-parser-js) >=0.7.4 212 | 213 | 214 | ## 💬 Contributing 215 | * [Code of Conduct](https://github.com/nhn/tui.app-loader/blob/production/CODE_OF_CONDUCT.md) 216 | * [Contributing guideline](https://github.com/nhn/tui.app-loader/blob/production/CONTRIBUTING.md) 217 | * [Issue guideline](https://github.com/nhn/tui.app-loader/blob/production/docs/ISSUE_TEMPLATE.md) 218 | * [Commit convention](https://github.com/nhn/tui.app-loader/blob/production/docs/COMMIT_MESSAGE_CONVENTION.md) 219 | 220 | 221 | ## 🍞 TOAST UI Family 222 | 223 | * [TOAST UI Editor](https://github.com/nhn/tui.editor) 224 | * [TOAST UI Calendar](https://github.com/nhn/tui.calendar) 225 | * [TOAST UI Chart](https://github.com/nhn/tui.chart) 226 | * [TOAST UI Image-Editor](https://github.com/nhn/tui.image-editor) 227 | * [TOAST UI Grid](https://github.com/nhn/tui.grid) 228 | * [TOAST UI Components](https://github.com/nhn) 229 | 230 | 231 | ## 📜 License 232 | 233 | This software is licensed under the [MIT](https://github.com/nhn/tui.app-loader/blob/production/LICENSE) © [NHN Cloud](https://github.com/nhn). 234 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tui-app-loader", 3 | "authors": [ 4 | "NHN Cloud. FE Development Lab " 5 | ], 6 | "main": [ 7 | "dist/tui-app-loader.js" 8 | ], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "examples", 15 | "src", 16 | "karma.conf.js", 17 | "package-lock.json", 18 | "package.json", 19 | "tui-note.config.js", 20 | "tuidoc.config.json", 21 | "webpack.config.js" 22 | ], 23 | "dependencies": { 24 | "ua-parser-js": "^0.7.14" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/COMMIT_MESSAGE_CONVENTION.md: -------------------------------------------------------------------------------- 1 | # Commit Message Convention 2 | 3 | ## Commit Message Format 4 | 5 | ``` 6 | : Short description (fix #1234) 7 | 8 | Longer description here if necessary 9 | 10 | BREAKING CHANGE: only contain breaking change 11 | ``` 12 | * Any line of the commit message cannot be longer 100 characters! 13 | 14 | ## Revert 15 | ``` 16 | revert: commit 17 | 18 | This reverts commit 19 | More description if needed 20 | ``` 21 | 22 | ## Type 23 | Must be one of the following: 24 | 25 | * **feat**: A new feature 26 | * **fix**: A bug fix 27 | * **docs**: Documentation only changes 28 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 29 | * **refactor**: A code change that neither fixes a bug nor adds a feature 30 | * **perf**: A code change that improves performance 31 | * **test**: Adding missing or correcting existing tests 32 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 33 | 34 | ## Subject 35 | * use the imperative, __present__ tense: "change" not "changed" nor "changes" 36 | * don't capitalize the first letter 37 | * no dot (.) at the end 38 | * reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`. 39 | 40 | ## Body 41 | 42 | * use the imperative, __present__ tense: "change" not "changed" nor "changes". 43 | * the motivation for the change and contrast this with previous behavior. 44 | 45 | ## BREAKING CHANGE 46 | * This commit contains breaking change(s). 47 | * start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this. 48 | 49 | This convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2) 50 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 27 | 28 | ### Please check if the PR fulfills these requirements 29 | - [ ] It's submitted to right branch according to our branching model 30 | - [ ] It's right issue type on title 31 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) 32 | - [ ] The commit message follows our guidelines 33 | - [ ] Tests for the changes have been added (for bug fixes/features) 34 | - [ ] Docs have been added/updated (for bug fixes/features) 35 | - [ ] It does not introduce a breaking change or has description for the breaking change 36 | 37 | ### Description 38 | 39 | 40 | 41 | --- 42 | Thank you for your contribution to TOAST UI product. 🎉 😘 ✨ 43 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Tutorials 2 | 3 | - [Getting Started](getting-started.md) 4 | - [v2.0.0 Migration Guide](v2.0.0-migration-guide.md) 5 | 6 | ## Documents 7 | 8 | - [Code of Conduct](../CODE_OF_CONDUCT.md) 9 | - [Contributing Guide](../CONTRIBUTING.md) 10 | - [Commit Message Convention](COMMIT_MESSAGE_CONVENTION.md) 11 | - [API & Examples](https://nhn.github.io/tui.app-loader/latest) 12 | 13 | ## Issues 14 | - Firefox for android: Intent URI지원 - 41버전 (2015, 9) ([ref.](https://www.mozilla.org/en-US/firefox/android/41.0/releasenotes/)) 15 | - [Chromium의 intentURI와 iframe](issue-iframe.md) 16 | - [iOS9 앱 로드시 얼럿, 스토어이동 관련 이슈](issue-iOS9.md) 17 | - [앱 로드 후 기존 페이지 유지 관련 이슈](issue-remain-page.md) 18 | - [안드로이드 크롬 버전 별 이슈](issue-chrome.md) 19 | - [웹뷰에서 앱 로더 수행되지 않는 이슈](issue-webview.md) 20 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ``` sh 4 | $ npm install --save tui-app-loader # Latest version 5 | $ npm install --save tui-app-loader@ # Specific version 6 | ``` 7 | 8 | It can also be installed by using bower or downloaded by CDN. Please refer to the [💾 Install](https://github.com/nhn/tui.app-loader#-install). 9 | 10 | ## Usage 11 | 12 | ```javascript 13 | import AppLoader from 'tui-app-loader'; 14 | 15 | const appLoader = new AppLoader(); 16 | appLoader.exec({ ... }); 17 | ``` 18 | 19 | It can also be used by namespace or CommonJS module. Please refer to the [🔨 Usage](https://github.com/nhn/tui.app-loader#-usage). 20 | 21 | ## Set options for landing scheme 22 | AppLoader runs on a iOS or Android device. 23 | Open an app when an app is installed, otherwise open an website. 24 | As an Android and iOS have a different deep-linking policy, you should set both scheme. 25 | 26 | ### iOS 27 | * [`Universal link`](https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html) for `iOS9+`(**Recommended**): don't need to set an fallback url by `ios.url`. because universal link handles a fallback url. Unlike custom URL scheme, universal link is unique for app. 28 | * `Custom URL scheme` for `<=iOS8`: need to set an fallback url by `ios.url`. 29 | 30 | ### Android 31 | * [`intentURI`](https://developer.chrome.com/multidevice/android/intents) for `Chromium 25+`(**Recommended**): don't need to set an fallback url, you could set fallback url by adding `S.browser_fallback_url=[encoded_full_url]` in intentURI. 32 | * `urlScheme`: need to set an fallback url. AppLoader will land this scheme on an Android device, when `android.intentURI` exists, but `ios.scheme` option doesn't. 33 | 34 | | option | Environment | description | 35 | | -- | -- | -- | 36 | | ios.universalLink | iOS9+ | recommended | 37 | | ios.scheme* | <=iOS8 | | 38 | | ios.url | <=iOS8 | iOS fallback url of `ios.scheme`| 39 | | android.intentURI | Android/Chromium25+ | recommended | 40 | | notFoundCallback | all | Android fallbackURL, works when use `ios.scheme`
default: move to iOS store | 41 | | etcCallback | !iOS && !Android | | 42 | | timerSet | all | delay time(ms) on app loading
default: iOS(2000), Android(800) | 43 | 44 | \* - can be used as a replacement of **android.intentURI**. Android use it as a custom URI scheme. 45 | 46 | ## Example code 47 | 48 | ```javascript 49 | const appLoader = new AppLoader(); 50 | appLoader.exec({ 51 | ios: { 52 | scheme: '://', 53 | url: '', // app store url 54 | }, 55 | android: { 56 | intentURI: 'intent://#Intent;scheme=;package=;S.browser_fallback_url=;end' 57 | }, 58 | notFoundCallback: function(storeUrl) { 59 | alert('could not find app'); 60 | }, 61 | etcCallback: function() { 62 | alert('support iOS and Android only'); 63 | } 64 | }); 65 | ``` 66 | 67 | For more information about the API, please see [here](https://nhn.github.io/tui.app-loader/latest/AppLoader). 68 | -------------------------------------------------------------------------------- /docs/issue-chrome.md: -------------------------------------------------------------------------------- 1 | (2016. 1. 20 업데이트) 2 | # 안드로이드 크롬 지원 기능 정리 3 | 4 | | 브라우저 | Iframe + scheme/intentURI | IntentURI | 마켓 이동 | Callback 지원 | Fallback URL 지원 | 5 | |---------------------------|------------------------|------------|---------|--------------|------------------| 6 | | Chrome for Android 25~42 | X | O | O | X* | X* | 7 | | Chrome for Android 42~ | O* | O | O | O | O | 8 | | 웹뷰 기본 (커스터마이징X) | X | X | X | X | X | 9 | | 웹뷰 커스터마이징 | ? | ? | ? | ? | ? | 10 | 11 | _( X* : 기본적으로 컴포넌트에서 지원할 수 없으나, 특정 조건에 대한 우회방안은 있음)_ 12 | 13 | _( ? : 웹뷰 커스터마이징에 따라 지원될 수 있음 — 해당 어플리케이션 내부 웹뷰의 URI 핸들링방식에 따라 지원 가능)_ 14 | 15 | ___( O* :
iframe을 body에 붙이기 전 src속성을 intentURI로 지정하면 동작함
body에 붙인 이후 src속성을 변경하면 동작하지 않음 )___ 16 | 17 | 18 | **참고: Kitkat(android 4.4) 부터는 기본 WebView가 Chromium 30 기반으로 도는 것 같아요. userAgnet 값에도 `Chrome/30.0.0.0`이 찍히구요. android 버전 4.4 이상이 50%가 넘어가는 상황이라 facebook, Naver, Daum 브라우져등에서 쓰는 기본웹뷰들은 Chromium으로 봐도 되지 않을까 싶네요. Rollipop부터는 Chromium 37+ 인 것 같구요..** 19 | # 이슈 20 | 21 | * 안드로이드의 기본 웹뷰에서는 앱로더의 기능은 전부 지원되지 않는다. 22 | * [안드로이드 웹뷰에서 앱 로더가 수행되지 않는 이슈](https://github.com/nhn/tui.app-loader/wiki/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9B%B9%EB%B7%B0%EC%97%90%EC%84%9C-%EC%95%B1%EB%A1%9C%EB%8D%94%EA%B0%80-%EC%88%98%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%8A%88) 23 | 24 | * Chrome for Android 25~42에서 callback과 fallback url지원은 *팝업을 통한 우회가 가능*하다. 25 | * [안드로이드에서 app 실행 또는 fallback url 기능 지원](https://github.com/nhn/tui.app-loader/wiki/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-APP-%EC%8B%A4%ED%96%89-%EB%98%90%EB%8A%94-%ED%8A%B9%EC%A0%95-url%EB%A1%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9D%B4%EB%8F%99) 26 | 27 | * IntentURI의 iframe지원? 28 | * [Chromium의 intentURI와 iframe](https://github.com/nhn/tui.app-loader/wiki/Chromium%EC%9D%98-intentURI%EC%99%80-iframe) 29 | 30 | # 관련 참고 링크 31 | 32 | **Intent URI** 33 | * 가이드 페이지 - https://developer.chrome.com/multidevice/android/intents 34 | 35 | **크롬 25 ~ 42 버전 관련 이슈** 36 | * intentURI가 올바르지 않을 때 발생하는 페이지 깨짐(ERR_UNKNOWN_URL_SCHEME) 37 | * 크롬 이슈 38 | * https://code.google.com/p/chromium/issues/detail?id=331571 39 | * https://code.google.com/p/chromium/issues/detail?id=477456 40 | * 이슈 해설 - https://paul.kinlan.me/deep-app-linking-on-android-and-chrome/ 41 | -------------------------------------------------------------------------------- /docs/issue-iOS9.md: -------------------------------------------------------------------------------- 1 | HTML 2 | ```html 3 | 4 | ``` 5 | 6 | Javascript 7 | ```js 8 | var iosScheme = 'payco://open'; 9 | var intentURI = 'intent://open#Intent;scheme=payco;package=com.nhnent.payapp;end;'; 10 | var AppLoader = tui.component.m.AppLoader; 11 | var loader = new AppLoader(); 12 | var btn = document.getElementById('loaderBtn'); 13 | 14 | btn.addEventListener('click', function() { 15 | loader.exec({ 16 | ios: { 17 | scheme: iosScheme, 18 | url: "https://itunes.apple.com/kr/app/id924292102", 19 | useIOS9: true, 20 | syncToIOS9 : false 21 | }, 22 | android: { 23 | intentURI: intentURI 24 | }, 25 | etcCallback: function() { 26 | if (AppLoader.getOS() !== 'iOS' && AppLoader.getOS() !== 'Android') { 27 | alert('Run in mobile'); 28 | } 29 | }, 30 | notFoundCallback: function() { 31 | console.log('Not Found Application'); 32 | } 33 | }); 34 | }); 35 | ``` 36 | 37 | ### 첫번째 시나리오 이슈 (앱 설치, Timer 2초) 38 | 1. Load버튼을 누름 39 | 2. 앱 실행 확인창 40 | 3. "열기" 41 | 4. 앱이 열리는 순간 "safari로 돌아가기" 누름 (AppLoader에 설정한 timer 시간 이내 이내 (기본값: 2초)) 42 | 5. ***앱이 설치되어있음에도 불구하고 app store로 이동되어버림*** 43 | 44 | ### 두번째 시나리오 이슈 (앱 미설치, Timer 2초) 45 | 1. Load버튼을 누름 46 | 2. `페이지를 열 수 없음` 얼럿창 47 | 3. "승인" 누름 48 | 4. `'App Store'에서 이 페이지를 열겠습니까?` <-- 얼럿창이 나타남 49 | 5. "취소" 누름 50 | 6. 다음에 다시 버튼을 눌러도 앱 스토어 이동이 되지 않음. 51 | 52 | ### 세번째 시나리오 이슈 (앱 미설치, Timer 1초) 53 | * 첫번째 이슈로 인해 `AppLoader의 timer시간을 1초로 줄이게 됨` 54 | 55 | ```js 56 | timerSet: { 57 | ios: 1000 58 | }, 59 | ``` 60 | 61 | 1. Load버튼을 누름 62 | 2. `페이지를 열 수 없음` 얼럿창 63 | 3. "승인" 누름 64 | 4. `'App Store'에서 이 페이지를 열겠습니까?` <-- ***얼럿창이 나타나지 않음*** 65 | * 로드 버튼을 눌렀을때의 사용자 액션을 기억하고있는 것으로 추측됨 66 | * Timer가 2초인 경우 `'App Store'에서 이 페이지를 열겠습니까?`얼럿창이 나타남 67 | 5. 앱스토어로 이동됨. 68 | 69 | ### 네번째 시나리오 이슈 (앱 미설치, Timer 1초) 70 | 1. `1`~`3`까지는 두번째 시나리오와 동일 71 | 2. "승인"을 누르지 않고 기다림 72 | 3. 앱스토어로 이동됨 73 | 74 | 참고: Timer가 2초인 경우에는 앱스토어 이동 확인 얼럿창이 뜨기때문에 바로 이동되지 않음 75 | 76 | ### 결론 77 | Timer를 1초로 해도 이슈가 있고, 2초로 해도 이슈가 있음. 78 | 79 | ### 우회방안 80 | 1. Timer설정은 따로 하지 않아도 됨. 81 | 2. `notFoundCallback`은 아무 동작도 하지 않도록 지정 82 | 83 | ```js 84 | notFoundCallback: function() {} 85 | ``` 86 | 3. 앱 스토어 이동 버튼을 따로 추가 87 | 88 | ```html 89 | 90 | 91 | ``` 92 | -------------------------------------------------------------------------------- /docs/issue-iframe.md: -------------------------------------------------------------------------------- 1 | #### intentURI의 스펙은 공식적으로 iframe을 지원하지 않는다고 명시되어있음. 2 | 3 | 1. 현재 Chromium 42버전(추정)이상부터 iframe을 통해 app 호출이 가능함. (Chromium-47까지 확인) 4 | 5 | 2. 언제 iframe을 사용하는 방식이 막힐지는 알 수 없음. 6 |
따라서 컴포넌트가 지원하는 iframe방식은 비공식적인 방식 7 |
`useIframe: true`옵션을 통해 적용 가능 8 | 9 | 3. iframe관련 이슈가 있음. 10 |
iframe이 document에 추가된 이후 src속성을 변경하는 경우에는 app 수행이 차단됨 11 |
iframe방식에서 src를 먼저 셋팅하고 그 이후 body에 append시키면 app수행이 됨. ___(다시 한번 강조 - 모든 버전이 되는것은 아님)___ 12 |
![iframe](https://cloud.githubusercontent.com/assets/12269563/12475773/7866eb2a-c068-11e5-945f-4c079be67817.jpeg) 13 | 14 | 4. iframe을 통해 호출하는 경우 `S.browser_fallback_url` 옵션 파라메타는 적용되지 않음 15 |
대안으로 컴포넌트에서 `notFoundCallback` 함수를 입력받음. (ios에서 notFound인 경우에도 발생함) 16 |
25~42 버전까지는 iframe방식에서 app의 설치유무를 판단할 수 없기때문에 notFoundCallback을 호출할 수 없음 17 |
대신 `onErrorIframe` 함수를 입력받아 수행함. 18 | -------------------------------------------------------------------------------- /docs/issue-remain-page.md: -------------------------------------------------------------------------------- 1 | ### iOS9 : 팝업창에서 앱 로드 이슈 2 | 3 | 페이지(1) 로드 -> 팝업(2) 로드 -> 팝업(2)에서 버튼 클릭 -> iOS scheme을 통해 앱 로드 4 | 기존 페이지(1)은 유지됨. 5 | 팝업(2)가 닫혀버림. 6 | 7 | * iOS9에서 위의 이슈 확인 8 | * 팝업에서 앱을 로드하고싶은 경우 9 | * ``태그의 `href`속성에 iOS scheme를 셋팅 10 | * 또는 버튼 클릭시 이벤트에서 `window.open(iOS scheme);`을 통해 또다른 팝업(3)을 호출 11 | * 참고 12 | * **팝업에서의 로드가 아닌 경우는 기존 페이지는 그대로 유지됨.** 13 | 14 | ### 안드로이드 : Chrome에서 페이지 로드 후 사용자 액션 없이 바로 앱 로드 이슈 15 | 16 | 페이지(1) 로드 -> 사용자 액션 없이 바로 intentURI을 통해 앱을 로드 17 | 기존 페이지(1)이 팝업일 경우 팝업이 닫히고, 일반 바닥-페이지의 경우 history back현상 발생함. 18 | 19 | * 삼성브라우저 Chromium(34) 20 | * 페이지 로드에서 바로 앱을 로드 -> 기존 페이지는 사라지거나 뒤로 돌아가지 않음 21 | * 최신 크롬 22 | * 페이지 로드에서 바로 앱을 로드 -> 기존 페이지가 사라지거나 뒤로 돌아감 23 | * 결론 24 | * `버튼 클릭 -> 앱 로드` 순서로 가면 둘다 기존 페이지 유지됨. 25 | * 참고 26 | * iOS는 기존 페이지가 그대로 남아있음. 27 | -------------------------------------------------------------------------------- /docs/issue-webview.md: -------------------------------------------------------------------------------- 1 | # 요약 2 | **웹뷰에서 앱로더가 동작하지 않는 이슈는 프론트엔드에서 해결할 수 없고, 어플리케이션에서 직접 핸들링해 주어야 함.** 3 | 4 | # 원인 5 | 1. 안드로이드 버전 4.4부터 웹뷰가 Chromium 기반으로 변경. 6 | (https://developer.android.com/intl/ko/guide/webapps/migrating.html) 7 | 8 | 2. 그래서 User-Agent에 "Chrome" 이 포함되기 때문에, 프론트에서는 크롬 브라우저라고 판단. 9 | 10 | 3. 현재 Chromium기반 웹뷰는 Chrome for Android의 스펙인 IntentURI를 지원하지 않음. 11 | (IntentURI - https://developer.chrome.com/multidevice/android/intents) 12 | 13 | 4. 따라서 기존의 앱로더처럼 동작을 수행하기 위해서는 어플리케이션 내부 코드를 변경. 14 | 15 | 5. 어플리케이션 내부 코드에서, 웹뷰로 들어가는 URI를 파싱하여 intentURI인 경우 따로 처리하는 로직이 필요. 16 | 17 | 6. 이러한 웹뷰의 경우 커스터마이징 요소들이 많기때문에 프론트엔드에서 지원할 수 있는 범위가 아니라고 판단됨. 18 | 19 | 20 | # 해결 21 | 22 | 이번 이슈는 안드로이드 어플리케이션 프로토타이핑을 통해 아래와 같은 웹뷰클라이언트 구현으로 URI를 핸들링하였으며, 23 | 24 | 테스트를 통해 웹뷰 내부 앱로더의 정상 동작을 확인하였음. 25 | 26 | ```java 27 | public class MyWebViewClient extends WebViewClient { 28 | public static final String INTENT_URI_START = "intent:"; 29 | public static final String INTENT_FALLBACK_URL = "browser_fallback_url"; 30 | public static final String URI_SCHEME_MARKET = "market://details?id="; 31 | 32 | @Override 33 | public boolean shouldOverrideUrlLoading(WebView view, String uri) { 34 | if (uri.toLowerCase().startsWith(INTENT_URI_START)) { 35 | Intent parsedIntent = null; 36 | try { 37 | parsedIntent = Intent.parseUri(uri, 0); 38 | startActivity(parsedIntent); 39 | } catch(ActivityNotFoundException | URISyntaxException e) { 40 | return doFallback(view, parsedIntent); 41 | } 42 | } else { 43 | view.loadUrl(uri); 44 | } 45 | return true; 46 | } 47 | 48 | public boolean doFallback(WebView view, Intent parsedIntent) { 49 | if (parsedIntent == null) { 50 | return false; 51 | } 52 | 53 | String fallbackUrl = parsedIntent.getStringExtra(INTENT_FALLBACK_URL); 54 | if (fallbackUrl != null) { 55 | view.loadUrl(fallbackUrl); 56 | return true; 57 | } 58 | 59 | String packageName = parsedIntent.getPackage(); 60 | if (packageName != null) { 61 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URI_SCHEME_MARKET + packageName))); 62 | return true; 63 | } 64 | return false; 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/v2.0.0-migration-guide.md: -------------------------------------------------------------------------------- 1 | ## External Dependency: ua-parser-js@^0.7.1 2 | 3 | AppLoader has `external` dependency: `ua-parser-js`. 4 | It means you should add this dependency before loading `tui-app-loader.js`. 5 | 6 | ```html 7 | 8 | 9 | ``` 10 | 11 | ### External Dependency: tui-app-loader <=2.1.4 12 | 13 | AppLoader below v2.1.4 has additional `external` dependency: `tui-code-snippet@1.5.2`. 14 | If you use AppLoader below v2.1.4, you should add `tui-code-snippet` before loading `tui-app-loader.js`. 15 | 16 | ```html 17 | 18 | 19 | 20 | ``` 21 | 22 | ## Deprecate Parser APIs: 23 | 24 | AppLoader had a code dependency inside a module. 25 | So we provide Parser APIs for users who need User Agent information like OS and browser name and version. 26 | 27 | Now, in a v2.0.0, we exclude a dependency outside of a module. 28 | If user want to know User Agents, they can use a parser package, `ua-parser-js`. 29 | This is why we deprecated Parser APIs. Use `ua-parser-js`. AppLoader uses the same package. 30 | 31 | Deprecated APIs 32 | - AppLoader.getOS() 33 | - AppLoader.getUserAgent(): {string} 34 | - AppLoader.getUserAgents(): {Array} 35 | - AppLoader.getVersion(): {number|string} 36 | - new AppLoader().getOS(): {string} 37 | - new AppLoader().IsIntentLess(): {boolean} 38 | 39 | \* - Check a browser is not a Firefox or a Opera. This can be implemented by `new UAParser().getResult().os.name` 40 | 41 | ## Changed naming policies 42 | 43 | | | v2.0.0 | 2 | 3 | 4 | 5 | 1. Basic - launch App by clicking a button 6 | 7 | 18 | 19 | 20 |
21 | AppLoader runs on Android or iOS device. 22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/example02-using-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2. Using iframe - Launch App with page loading 6 | 7 | 8 | 9 |
10 | AppLoader runs on Android or iOS device.
11 | Due to the
policy of the mobile Chrome, it may not launch an app automatically when you use tui-app-loader in the iframe. It is recommended to put a button in case tui-app-loader does not work properly. See here for an example of using a button. 12 |
13 | 14 | 15 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tui-app-loader", 3 | "version": "2.1.6", 4 | "main": "dist/tui-app-loader", 5 | "files": [ 6 | "dist" 7 | ], 8 | "scripts": { 9 | "bundle": "webpack --mode=production & webpack --mode=production --minify", 10 | "serve": "webpack-dev-server --inline --hot -d", 11 | "serve:ie8": "webpack-dev-server -d", 12 | "doc:serve": "tuidoc --serv", 13 | "doc": "tuidoc", 14 | "note": "tui-note" 15 | }, 16 | "description": "TOAST UI Component: AppLoader for mobile deep-linking", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/nhn/tui.app-loader.git" 20 | }, 21 | "keywords": [ 22 | "nhn", 23 | "tui", 24 | "deep linking", 25 | "app-loader", 26 | "app launcher", 27 | "nhn cloud" 28 | ], 29 | "author": "NHN Cloud. FE Development Lab ", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@toast-ui/release-notes": "^2.0.1", 33 | "eslint": "^5.12.0", 34 | "eslint-config-prettier": "^6.2.0", 35 | "eslint-config-tui": "^2.2.0", 36 | "eslint-loader": "^3.0.0", 37 | "eslint-plugin-prettier": "^3.1.2", 38 | "prettier": "^1.18.2", 39 | "terser-webpack-plugin": "^2.2.3", 40 | "tui-code-snippet": "^2.2.0", 41 | "webpack": "^4.39.3", 42 | "webpack-cli": "^3.3.7", 43 | "webpack-dev-server": "^3.8.0" 44 | }, 45 | "dependencies": { 46 | "ua-parser-js": "^0.7.20" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/appLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Load native app or move to install page 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var defineClass = require('tui-code-snippet/defineClass/defineClass'); 8 | var extend = require('tui-code-snippet/object/extend'); 9 | var sendHostname = require('tui-code-snippet/request/sendHostname'); 10 | 11 | var UAParser = require('ua-parser-js'); 12 | var Detector = require('./detectors'); 13 | var iOSDetector = require('./iosDetectors'); 14 | var EtcDetector = require('./etcDetectors'); 15 | 16 | var defaultOptions = { 17 | ios: { 18 | scheme: '', 19 | url: '' 20 | }, 21 | android: { 22 | scheme: '', 23 | url: '' 24 | } 25 | }; 26 | 27 | /** 28 | * Mobile App loader 29 | * @constructor 30 | * @class 31 | * @param {object} options - Option object 32 | * @param {boolean} [options.usageStatistics=true] - Let us know the hostname. If you don't want to send the hostname, please set to false. 33 | * @see AppLoader#exec 34 | * @example node, commonjs 35 | * // ES6 36 | * import AppLoader from 'tui-app-loader'; // ES6 37 | * 38 | * // CommonJS 39 | * const AppLoader = require('tui-app-loader'); // CommonJS 40 | * 41 | * // Browser 42 | * const appLoader = new tui.AppLoader(); 43 | * 44 | * const appLoader = new AppLoader(); 45 | * appLoader.exec(...); 46 | */ 47 | var AppLoader = defineClass( 48 | /** @lends AppLoader.prototype */ { 49 | init: function(options) { 50 | var agent = new UAParser().getResult(); 51 | var os = agent.os; 52 | 53 | this.agent = agent; 54 | this.ua = agent.ua; 55 | this.osName = os.name; 56 | this.osVersion = os.version; 57 | this.detector = null; 58 | 59 | options = extend( 60 | { 61 | usageStatistics: true 62 | }, 63 | options 64 | ); 65 | 66 | if (options.usageStatistics) { 67 | sendHostname('app-loader', 'UA-129987462-1'); 68 | } 69 | }, 70 | 71 | /** 72 | * Set Detector by OS 73 | * @private 74 | * @param {object} context The options 75 | */ 76 | _setDetector: function(context) { 77 | var osName = this.osName; 78 | var isAndroid = osName === 'Android'; 79 | var isIOS = osName === 'iOS'; 80 | 81 | if (isAndroid) { 82 | this._setAndroidDetector(context); 83 | } else if (isIOS && context.iosStoreURL) { 84 | this._setIOSDetector(); 85 | } else { 86 | this._setEtcDetector(context); 87 | } 88 | }, 89 | 90 | /** 91 | * Set IOS Detector 92 | * @private 93 | * @param {object} context The information for app 94 | */ 95 | _setIOSDetector: function() { 96 | var iosVersion = parseInt(this.osVersion, 10); 97 | if (iosVersion > 8) { 98 | this.detector = iOSDetector.iOS9AndLater; 99 | } else if (iosVersion === 8) { 100 | this.detector = iOSDetector.iOS8; 101 | } else { 102 | this.detector = iOSDetector.iOS7AndBefore; 103 | } 104 | }, 105 | 106 | /** 107 | * Set android Detector 108 | * @private 109 | * @param {object} context The information for app 110 | */ 111 | _setAndroidDetector: function(context) { 112 | if (context.intentURI && this.doesBrowserSupportIntent()) { 113 | this.detector = Detector.androidIntentDetector; 114 | } else { 115 | this.detector = Detector.androidSchemeDetector; 116 | } 117 | }, 118 | 119 | /** 120 | * Set EtcDetector 121 | * @private 122 | * @param {object} context The information for app 123 | */ 124 | _setEtcDetector: function(context) { 125 | this.detector = EtcDetector; 126 | 127 | setTimeout(function() { 128 | if (context.etcCallback) { 129 | context.etcCallback(); 130 | } 131 | }, 100); 132 | }, 133 | 134 | /** 135 | * Run selected detector 136 | * @private 137 | * @param {object} context The information for app 138 | */ 139 | _runDetector: function(context) { 140 | if (this.detector && this.detector !== EtcDetector) { 141 | this.detector.run(context); 142 | } 143 | }, 144 | 145 | /** 146 | * Whether the intent is supported 147 | * @returns {boolean} 148 | * @private 149 | */ 150 | doesBrowserSupportIntent: function() { 151 | return !/firefox|opr/i.test(this.ua); 152 | }, 153 | 154 | /** 155 | * Call app 156 | * @param {object} options The option for app 157 | * @param {object} options.ios IOS app information 158 | * @param {object} options.android Android information 159 | * @param {object} options.timerSet A timer time set for callback deley time 160 | * @param {Function} options.etcCallback If unsupportable mobile 161 | * @param {Function} options.notFoundCallback It not found 162 | * 163 | * @example 164 | * const loader = new AppLoader(); 165 | * loader.exec({ 166 | * ios: { 167 | * scheme: '://', // iphone app scheme 168 | * url: 'https://itunes.apple.com/app/', // app store url, 169 | * universalLink: 'app:////' 170 | * }, 171 | * android: { 172 | * intentURI: 'intent://#Intent;scheme=;package=;end' // android intent uri 173 | * }, 174 | * timerSet: { // optional values 175 | * ios: 2000, // default: 2000 176 | * android: 1000 // default: 800 177 | * }, 178 | * notFoundCallback: function() { // if not installed 179 | * alert('not found'); 180 | * }, 181 | * etcCallback: function() { // if not mobile 182 | * alert('etc'); 183 | * } 184 | * }); 185 | */ 186 | exec: function(options) { 187 | var timerSet, context; 188 | 189 | options = extend(defaultOptions, options); 190 | timerSet = options.timerSet; 191 | context = { 192 | urlScheme: options.ios.scheme, 193 | iosStoreURL: options.ios.url, 194 | universalLink: options.ios.universalLink, 195 | intentURI: options.android.intentURI, 196 | useIframe: options.android.useIframe, 197 | onErrorIframe: options.android.onErrorIframe, 198 | etcCallback: options.etcCallback, 199 | notFoundCallback: options.notFoundCallback 200 | }; 201 | 202 | this._setDetector(context); 203 | if (timerSet) { 204 | this._setTimerTime(timerSet); 205 | } 206 | this._runDetector(context); 207 | }, 208 | 209 | /** 210 | * Set timer time set 211 | * @param {object} timerSet A set of timer times 212 | * @private 213 | */ 214 | _setTimerTime: function(timerSet) { 215 | if (!this.detector.TIMEOUT) { 216 | this.detector.TIMEOUT = {}; 217 | } 218 | this.detector.TIMEOUT.IOS = timerSet.ios || this.detector.TIMEOUT.IOS; 219 | this.detector.TIMEOUT.ANDROID = timerSet.android || this.detector.TIMEOUT.ANDROID; 220 | } 221 | } 222 | ); 223 | 224 | module.exports = AppLoader; 225 | -------------------------------------------------------------------------------- /src/js/detectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Mixin modules 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var extend = require('tui-code-snippet/object/extend'); 8 | var isExisty = require('tui-code-snippet/type/isExisty'); 9 | var isFunction = require('tui-code-snippet/type/isFunction'); 10 | var bind = require('./util').bind; 11 | 12 | var ID_SUPPORT_FRAME = 'tui-support-frame'; 13 | 14 | /** 15 | * @namespace Detector 16 | * @ignore 17 | */ 18 | var Detector = { 19 | /** 20 | * for timer 21 | */ 22 | TIMEOUT: { 23 | IOS: 2000, 24 | ANDROID: 800, 25 | INTERVAL: 100 26 | }, 27 | 28 | /** 29 | * Move page 30 | * @param {string} url - URL 31 | * @memberof Detector 32 | */ 33 | moveTo: function(url) { 34 | top.location.href = url; 35 | }, 36 | 37 | /** 38 | * Call app by iframe 39 | * @param {string} url - App url 40 | * @returns {HTMLElement} IFrame 41 | */ 42 | runAppWithIframe: function(url) { 43 | var iframe = this.createIFrameElement(); 44 | 45 | iframe.src = url; 46 | document.body.appendChild(iframe); 47 | 48 | return iframe; 49 | }, 50 | 51 | /** 52 | * Create iframe 53 | * @returns {HTMLElement} IFrame 54 | */ 55 | createIFrameElement: function() { 56 | var iframe = document.createElement('iframe'); 57 | iframe.id = ID_SUPPORT_FRAME; 58 | iframe.frameborder = '0'; 59 | iframe.width = '0'; 60 | iframe.height = '0'; 61 | iframe.style.display = 'none'; 62 | 63 | return iframe; 64 | }, 65 | 66 | /** 67 | * Defer call callback 68 | * @param {function} callback A callback 69 | * @param {number} time A delay time 70 | * @returns {number|undefined} Timer id 71 | */ 72 | deferCallback: function(callback, time) { 73 | var clickedAt = new Date().getTime(); 74 | var timer; 75 | 76 | if (isFunction(callback)) { 77 | timer = setTimeout( 78 | bind(function() { 79 | var now = new Date().getTime(); 80 | if (this.isPageVisible() && now - clickedAt < time + this.TIMEOUT.INTERVAL) { 81 | callback(); 82 | } 83 | }, this), 84 | time 85 | ); 86 | } 87 | 88 | return timer; 89 | }, 90 | 91 | /** 92 | * check a webpage is visible or in focus 93 | * @returns {boolean} Page visibility 94 | */ 95 | isPageVisible: function() { 96 | if (isExisty(document.hidden)) { 97 | return !document.hidden; 98 | } 99 | if (isExisty(document.webkitHidden)) { 100 | return !document.webkitHidden; 101 | } 102 | 103 | return true; 104 | } 105 | }; 106 | 107 | /**************** 108 | * Android series 109 | ****************/ 110 | 111 | /** 112 | * Android intent less 113 | * @namespace Detector.androidSchemeDetector 114 | * @ignore 115 | */ 116 | Detector.androidSchemeDetector = extend( 117 | { 118 | /** 119 | * detector type 120 | * @memberof Detector.androidSchemeDetector 121 | */ 122 | type: 'scheme', 123 | 124 | /** 125 | * Run detector 126 | * @deprecated 127 | * @param {object} context - Data for running 128 | * @memberof Detector.androidSchemeDetector 129 | */ 130 | run: function(context) { 131 | var notFoundCallback = context.notFoundCallback; 132 | 133 | if (notFoundCallback) { 134 | this.deferCallback(notFoundCallback, this.TIMEOUT.ANDROID); 135 | } 136 | this.runAppWithIframe(context.urlScheme); 137 | } 138 | }, 139 | Detector 140 | ); 141 | 142 | /** 143 | * Android intent 144 | * @namespace Detector.androidIntentDetector 145 | * @ignore 146 | */ 147 | Detector.androidIntentDetector = extend( 148 | { 149 | /** 150 | * detector type 151 | * @memberof Detector.androidIntentDetector 152 | */ 153 | type: 'intent', 154 | 155 | // Force iframe 156 | launchViaIframe: function(intentURI, notFoundCallback, onErrorIframe) { 157 | var iframe = this.runAppWithIframe(intentURI), // Launch app via iframe 158 | timeoutId = this.deferCallback(notFoundCallback, this.TIMEOUT.ANDROID); 159 | 160 | setTimeout(function() { 161 | try { 162 | // Whether broswer supports intentURI with iframe and without error. 163 | if (iframe && iframe.contentDocument.body) { 164 | document.body.removeChild(iframe); 165 | } 166 | } catch (e) { 167 | // If browser caught an error(accessing to error page in iframe), 168 | // this component cannot judge the app is installed or not. 169 | document.body.removeChild(iframe); 170 | clearTimeout(timeoutId); 171 | if (isFunction(onErrorIframe)) { 172 | onErrorIframe(); 173 | } 174 | } 175 | }, 100); 176 | }, 177 | 178 | /** 179 | * Run detector 180 | * @param {object} context - Data for running 181 | * @memberof Detector.androidIntentDetector 182 | * @ignore 183 | */ 184 | run: function(context) { 185 | var notFoundCallback = context.notFoundCallback, 186 | intentURI = context.intentURI; 187 | 188 | if (context.useIframe) { 189 | this.launchViaIframe(intentURI, notFoundCallback, context.onErrorIframe); 190 | } else { 191 | this.moveTo(intentURI); 192 | this.deferCallback(notFoundCallback, this.TIMEOUT.ANDROID); 193 | } 194 | } 195 | }, 196 | Detector 197 | ); 198 | 199 | module.exports = Detector; 200 | -------------------------------------------------------------------------------- /src/js/etcDetectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview EtcDetector for unsupported env. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * @namespace EtcDetector 9 | * @ignore 10 | */ 11 | var EtcDetector = { 12 | /** 13 | * @memberof EtcDetector 14 | */ 15 | type: 'etc', 16 | /** 17 | * @memberof EtcDetector 18 | */ 19 | run: function() {} 20 | }; 21 | module.exports = EtcDetector; 22 | -------------------------------------------------------------------------------- /src/js/iosDetectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview iOS Mixin modules 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var extend = require('tui-code-snippet/object/extend'); 8 | var bind = require('./util').bind; 9 | 10 | var Detector = require('./detectors'); 11 | 12 | /** 13 | * @namespace iOSDetector 14 | * @ignore 15 | */ 16 | var iOSDetector = extend( 17 | { 18 | /** 19 | * visiblitychange event 20 | * @memberof iOSDetector 21 | */ 22 | bindVisibilityChangeEvent: function() { 23 | document.addEventListener( 24 | 'visibilitychange', 25 | bind(function clear() { 26 | if (this.isPageVisible()) { 27 | clearTimeout(this.tid); 28 | document.removeEventListener('visibilitychange', clear); 29 | } 30 | }, this) 31 | ); 32 | }, 33 | 34 | /** 35 | * pagehide event 36 | * @memberof iOSDetector 37 | */ 38 | bindPagehideEvent: function() { 39 | window.addEventListener( 40 | 'pagehide', 41 | bind(function clear() { 42 | if (this.isPageVisible()) { 43 | clearTimeout(this.tid); 44 | window.removeEventListener('pagehide', clear); 45 | } 46 | }, this) 47 | ); 48 | } 49 | }, 50 | Detector 51 | ); 52 | 53 | /** 54 | * open an app on iOS7 and Before 55 | * @namespace iOSDetector.iOS7AndBefore 56 | * @ignore 57 | */ 58 | iOSDetector.iOS7AndBefore = extend( 59 | { 60 | /** 61 | * detector Run 62 | * @param {object} context Data for app loading 63 | * @memberof iOSDetector.iOS7AndBefore 64 | */ 65 | run: function(context) { 66 | var storeURL = context.iosStoreURL, 67 | callback = context.notFoundCallback || bind(this.moveTo, this, storeURL); 68 | 69 | this.tid = this.deferCallback(callback, this.TIMEOUT.IOS); 70 | this.bindPagehideEvent(); 71 | this.runAppWithIframe(context.urlScheme); 72 | } 73 | }, 74 | iOSDetector 75 | ); 76 | 77 | /** 78 | * ios recent detector 79 | * @namespace iOSDetector.iOS8 80 | * @ignore 81 | */ 82 | iOSDetector.iOS8 = extend( 83 | { 84 | /** 85 | * detector run 86 | * @param {object} context Data for app loading 87 | * @memberof iOSDetector.iOS8AndHigher 88 | */ 89 | run: function(context) { 90 | var storeURL = context.iosStoreURL, 91 | notFoundCallback = context.notFoundCallback, 92 | callback = notFoundCallback || bind(this.moveTo, this, storeURL); 93 | 94 | this.tid = this.deferCallback(callback, this.TIMEOUT.IOS); 95 | this.bindVisibilityChangeEvent(); 96 | this.runAppWithIframe(context.urlScheme); 97 | } 98 | }, 99 | iOSDetector 100 | ); 101 | 102 | /** 103 | * ios recent but safari prevent to call application via iframe src. 104 | * @ignore 105 | */ 106 | iOSDetector.iOS9AndLater = extend( 107 | { 108 | /** 109 | * detector run 110 | * @param {object} context Data for app loading 111 | * @memberof iOSDetector.iOS9AndLater 112 | */ 113 | run: function(context) { 114 | var storeURL = context.iosStoreURL, 115 | notFoundCallback = context.notFoundCallback, 116 | callback = notFoundCallback || bind(this.moveTo, this, storeURL); 117 | 118 | if (context.universalLink) { 119 | this.moveTo(context.universalLink); 120 | } else { 121 | this.tid = this.deferCallback(callback, this.TIMEOUT.IOS); 122 | this.bindVisibilityChangeEvent(); 123 | this.moveTo(context.urlScheme); 124 | } 125 | } 126 | }, 127 | iOSDetector 128 | ); 129 | 130 | module.exports = iOSDetector; 131 | -------------------------------------------------------------------------------- /src/js/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Utility 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var utils = { 8 | /** 9 | * Create a new function that, when called, has its this keyword set to the provided value. 10 | * @param {function} fn A original function before binding 11 | * @param {*} obj context of function in arguments[0] 12 | * @returns {function} A new bound function with context that is in arguments[1] 13 | */ 14 | bind: function(fn, obj) { 15 | var slice = Array.prototype.slice; 16 | var args; 17 | 18 | if (fn.bind) { 19 | return fn.bind.apply(fn, slice.call(arguments, 1)); 20 | } 21 | 22 | args = slice.call(arguments, 2); 23 | 24 | return function() { 25 | return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); 26 | }; 27 | } 28 | }; 29 | 30 | module.exports = utils; 31 | -------------------------------------------------------------------------------- /tui-note.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6 */ 2 | 3 | module.exports = { 4 | downloads: ({ name, version }) => { 5 | const extensions = ['.js', '.min.js']; 6 | const result = {}; 7 | 8 | extensions.forEach(ext => { 9 | const filename = name + ext; 10 | result[filename] = `https://uicdn.toast.com/${name}/v${version}/${filename}`; 11 | }); 12 | 13 | return result; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /tuidoc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "logo": { 4 | "src": "https://uicdn.toast.com/toastui/img/tui-component-bi-white.png" 5 | }, 6 | "title": { 7 | "text": "App Loader", 8 | "linkUrl": "https://github.com/nhn/tui.app-loader" 9 | } 10 | }, 11 | "footer": [ 12 | { 13 | "title": "NHN Cloud", 14 | "linkUrl": "https://github.com/nhn" 15 | }, 16 | { 17 | "title": "FE Development Lab", 18 | "linkUrl": "https://ui.toast.com/" 19 | } 20 | ], 21 | "main": { 22 | "filePath": "README.md" 23 | }, 24 | "api": { 25 | "filePath": "src/js/**", 26 | "permalink": true 27 | }, 28 | "examples": { 29 | "filePath": "examples", 30 | "titles": { 31 | "example01-basic": "1. Basic - launch App by clicking a button", 32 | "example02-using-iframe": "2. Using iframe - launch App with page loading" 33 | } 34 | }, 35 | "pathPrefix": "tui.app-loader" 36 | } 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configs file for bundling 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var path = require('path'); 8 | var pkg = require('./package.json'); 9 | var webpack = require('webpack'); 10 | var TerserPlugin = require('terser-webpack-plugin'); 11 | 12 | function getOptimization(isMinified) { 13 | if (isMinified) { 14 | return { 15 | minimizer: [ 16 | new TerserPlugin({ 17 | cache: true, 18 | parallel: true, 19 | sourceMap: false, 20 | extractComments: false 21 | }) 22 | ] 23 | }; 24 | } 25 | 26 | return { 27 | minimize: false 28 | }; 29 | } 30 | 31 | module.exports = function(env, argv) { 32 | var isProduction = argv.mode === 'production'; 33 | var isMinified = !!argv.minify; 34 | var FILENAME = pkg.name + (isMinified ? '.min' : ''); 35 | var BANNER = [ 36 | 'TOAST UI App Loader', 37 | '@version ' + pkg.version, 38 | '@author ' + pkg.author, 39 | '@license ' + pkg.license 40 | ].join('\n'); 41 | 42 | return { 43 | mode: isProduction ? 'production' : 'development', 44 | entry: './src/js/appLoader.js', 45 | output: { 46 | library: ['tui', 'AppLoader'], 47 | libraryTarget: 'umd', 48 | path: path.resolve(__dirname, 'dist'), 49 | publicPath: 'dist/', 50 | filename: FILENAME + '.js' 51 | }, 52 | externals: { 53 | 'ua-parser-js': { 54 | commonjs: 'ua-parser-js', 55 | commonjs2: 'ua-parser-js', 56 | amd: 'ua-parser-js', 57 | root: 'UAParser' 58 | } 59 | }, 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.js$/, 64 | exclude: /(test|node_modules|bower_components)/, 65 | loader: 'eslint-loader', 66 | enforce: 'pre', 67 | options: { 68 | failOnError: isProduction 69 | } 70 | } 71 | ] 72 | }, 73 | plugins: [new webpack.BannerPlugin(BANNER)], 74 | optimization: getOptimization(isMinified), 75 | devServer: { 76 | historyApiFallback: false, 77 | progress: true, 78 | host: '0.0.0.0', 79 | disableHostCheck: true 80 | } 81 | }; 82 | }; 83 | --------------------------------------------------------------------------------