├── .babelrc ├── .gitignore ├── .npmignore ├── .zuul.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── _config.yml ├── bin ├── deploy └── version ├── bower.json ├── css ├── animate.css ├── demo.css ├── demo.styl ├── index.styl ├── normalize.styl ├── social_icons.css └── transitions.css ├── examples └── bundling │ ├── browserify │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── index.js │ └── package.json │ └── webpack │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js ├── package.json ├── src ├── atom │ └── index.js ├── browser.js ├── cred │ ├── country_codes.js │ ├── email │ │ ├── actions.js │ │ ├── ask_email.jsx │ │ ├── email_input.jsx │ │ └── email_pane.jsx │ ├── index.js │ ├── input_wrap.jsx │ ├── or │ │ ├── ask_social_network_or_email.jsx │ │ └── ask_social_network_or_phone_number.jsx │ ├── phone-number │ │ ├── actions.js │ │ ├── ask_location.jsx │ │ ├── ask_phone_number.jsx │ │ ├── index.js │ │ ├── location_input.jsx │ │ ├── location_select.jsx │ │ ├── phone_number_input.jsx │ │ └── phone_number_pane.jsx │ ├── social │ │ ├── ask_social_network.jsx │ │ ├── social_button.jsx │ │ └── social_buttons_pane.jsx │ ├── storage.js │ └── vcode │ │ ├── actions.js │ │ ├── ask_vcode.jsx │ │ ├── vcode_input.jsx │ │ └── vcode_pane.jsx ├── dict │ ├── dicts.js │ ├── index.js │ └── t.js ├── gravatar │ ├── actions.js │ ├── index.js │ └── web_api.js ├── header │ ├── background.jsx │ ├── header.jsx │ ├── icon.jsx │ ├── welcome.jsx │ └── welcome_message.jsx ├── icon │ ├── button.jsx │ ├── icon.jsx │ └── index.js ├── index.js ├── lock │ ├── actions.js │ ├── avatar.jsx │ ├── badge.jsx │ ├── chrome.jsx │ ├── confirmation_pane.jsx │ ├── container_manager.js │ ├── global_error.jsx │ ├── index.js │ ├── lock.jsx │ ├── pane_separator.jsx │ ├── plugin_manager.js │ ├── render_scheduler.js │ ├── renderer.js │ ├── screen.js │ ├── signed_in_confirmation.jsx │ ├── submit_button.jsx │ ├── terms.jsx │ └── web_api.js ├── mode │ ├── emailcode │ │ └── spec.js │ ├── index.js │ ├── magiclink │ │ └── spec.js │ ├── sms │ │ └── spec.js │ ├── social-or-emailcode │ │ └── spec.js │ ├── social-or-magiclink │ │ └── spec.js │ ├── social-or-sms │ │ └── spec.js │ └── social │ │ └── spec.js ├── multisize-slide │ └── multisize_slide.js ├── passwordless │ ├── actions.js │ ├── ask_email.jsx │ ├── ask_email_vcode.jsx │ ├── ask_phone_number.jsx │ ├── ask_phone_number_vcode.jsx │ ├── ask_vcode.jsx │ ├── email_sent_confirmation.jsx │ ├── index.js │ └── magiclink.jsx ├── preload │ └── index.js ├── social │ ├── actions.js │ └── index.js ├── store │ └── index.js └── utils │ ├── esc_keydown_utils.js │ ├── fn_utils.js │ ├── id_utils.js │ ├── jsonp_utils.js │ ├── media_utils.js │ └── string_utils.js ├── support ├── design │ ├── control.jsx │ ├── index.html │ ├── index.js │ └── modal.html └── playground │ ├── assets │ └── remember.js │ ├── img │ └── logo-passwordless.svg │ ├── index.css │ ├── index.css.map │ ├── index.html │ ├── index.js │ └── index.scss └── test ├── acceptance_test_utils.js ├── cred └── index.test.js ├── gravatar ├── index.test.js └── web_api.test.js ├── index.test.js ├── lock ├── container_manager.test.js └── index.test.js ├── passwordless ├── emailcode.acceptance.test.js ├── magiclink.acceptance.test.js └── sms.acceptance.test.js └── utils ├── id_utils.test.js └── string_utils.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["version-inline", "transform-css-import-to-string"], 3 | "presets": ["es2015-loose", "stage-0", "react"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | css/index.css 4 | lib/ 5 | node_modules/ 6 | release/ 7 | npm-debug.log 8 | .css.map 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | css/ 4 | examples/ 5 | release/ 6 | src/ 7 | support/ 8 | test/ 9 | bower.json 10 | Gruntfile.js 11 | .css.map 12 | *~ 13 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | tunnel: 3 | type: ngrok 4 | authtoken: LsVZFxFqgxA4h7ibWV9V_iuA9afbQwaSnGqH9dApL 5 | browsers: 6 | - name: chrome 7 | version: latest 8 | - name: safari 9 | version: 8 10 | - name: firefox 11 | version: latest 12 | - name: ie 13 | version: 10..latest 14 | browserify: 15 | - transform: 16 | name: babelify 17 | - options: 18 | extensions: 19 | - .jsx 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.2.3] - 2016-03-28 2 | 3 | ### Fixed 4 | 5 | - Fix bug that prevented the widget to show up when a property was added to `String.prototype` ([#105](https://github.com/auth0/lock-passwordless/pull/105)) 6 | 7 | 8 | ## [2.2.2] - 2016-01-29 9 | 10 | ### Fixed 11 | 12 | - Fix webpack bundling. 13 | - Dependencies on the `0.14.x` range of React packages. Otherwise we are forcing users to use a specific version of React. 14 | 15 | ## [2.2.1] - 2016-01-18 16 | 17 | ### Changed 18 | 19 | - Some font alternatives are provided instead of defaulting to sans-serif immediately on systems without Avenir. 20 | 21 | ### Fixed 22 | 23 | - Pass `authParams` option in a social login. 24 | 25 | ## [2.2.0] - 2016-01-14 26 | 27 | ### Changed 28 | 29 | - Make it easy to bundle the module with [browserify](https://browserify.org) or [webpack](https://webpack.github.io/). Custom transforms or loaders are no longer needed. **Please review your bundling process when upgrading to this version**. 30 | 31 | ## [2.1.1] - 2016-01-07 32 | 33 | ### Fixed 34 | 35 | - Namespace style normalization rules inside .auth0-lock. 36 | 37 | ## [2.1.0] - 2016-01-06 38 | 39 | ### Changed 40 | 41 | - Allow spaces and hyphens in phone numbers. 42 | - Upgrade to React v0.14.5. 43 | - Upgrade to Auth0.js v6.8.0. 44 | 45 | ### Added 46 | 47 | - Add a back button when selecting a country code. 48 | 49 | ### Fixed 50 | 51 | - Force all characters to lower-case when creating the Gravatar's md5 hash. 52 | - Fix resend magic link button in applications using [page.js](https://github.com/visionmedia/page.js). 53 | 54 | ## [2.0.1] - 2015-12-04 55 | 56 | ### Fixed 57 | 58 | - Ensure Facebook page looks fine in the popup. 59 | 60 | ## [2.0.0] - 2015-12-03 61 | 62 | ### Changed 63 | 64 | - The Lock can now be reopened after it is closed. 65 | - When a `defaultLocation` option is not provided the country code will be derived from the user's geo-location. If it can't be obtained before the Lock is shown, it will default to _+1 (US)_. 66 | - Some dictionary keys, used for confirmation screens have been renamed: 67 | - `emailcode.confirmation` was changed to `emailcode.signedIn`. 68 | - `magiclink.confirmation` was changed to `magiclink.emailSent`. 69 | - `sms.confirmation` was changed to `sms.signedIn`. 70 | - Upgraded to React v0.14.3. Thanks @joelburget. 71 | - Upgraded to Auth0.js 6.7.7. 72 | 73 | ### Added 74 | 75 | - A warning will be shown when a `scope="openid profile"` is used. Can be avoided with the `disableWarnings` option. 76 | - Now the Lock allows to authenticate with social providers by calling the `social` method. The behavior can be controlled with the new `connections` and `socialBigButtons` options. 77 | - It is possible to mix social authentication with all the previously provided passwordless alternatives by calling `socialOrMagiclink`, `socialOrEmailcode` or `socialOrSms`. 78 | - A `destroy` method has been added since calling `close` no longer frees the Lock's resources. 79 | 80 | ### Fixed 81 | 82 | - Some styling tweaks in the Lock's header. 83 | - Footer text was displayed incorrectly in small screens. 84 | 85 | ## [1.0.2] - 2015-09-30 86 | 87 | ### Changed 88 | 89 | - Make phone number and verification code inputs bring up a numeric keyboard on mobile devices. 90 | 91 | ### Fixed 92 | 93 | - Debounce Gravatar requests. 94 | - Use always the same height for the header icon. 95 | - Ensure the title always shows up always in the same place. 96 | - Fix close and back buttons on some old IE versions, they weren't responding to the click event. 97 | - Ensure the Lock is displayed correctly in pages with z-ordered elements (as long as their values are less than 10.000.00). 98 | - Use colors taken from the Gravatar for the header background. 99 | 100 | ## [1.0.1] - 2015-09-30 101 | 102 | ### Added 103 | 104 | - Specific error message for invalid phone numbers. 105 | 106 | ### Fixed 107 | 108 | - Playground styling. 109 | 110 | ## [1.0.0] - 2015-09-30 111 | 112 | - First public release. 113 | 114 | ## [0.9.0] - 2015-09-30 115 | 116 | - First public pre-release. 117 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var pkg = require("./package"); 5 | 6 | var minor_version = pkg.version.replace(/\.(\d)*$/, ""); 7 | var major_version = pkg.version.replace(/\.(\d)*\.(\d)*$/, ""); 8 | var path = require("path"); 9 | 10 | function rename_release (v) { 11 | return function (d, f) { 12 | var dest = path.join(d, f.replace(/(\.min)?\.js$/, "-"+ v + "$1.js")); 13 | return dest; 14 | }; 15 | } 16 | 17 | module.exports = function(grunt) { 18 | 19 | grunt.initConfig({ 20 | pkg: grunt.file.readJSON("package.json"), 21 | aws_s3: { 22 | options: { 23 | accessKeyId: process.env.S3_KEY, 24 | secretAccessKey: process.env.S3_SECRET, 25 | bucket: process.env.S3_BUCKET, 26 | region: process.env.S3_REGION, 27 | uploadConcurrency: 5, 28 | params: { 29 | CacheControl: "public, max-age=300" 30 | }, 31 | // debug: true <<< use this option to test changes 32 | }, 33 | clean: { 34 | files: [ 35 | {action: "delete", dest: "js/lock-passwordless-" + pkg.version + ".js"}, 36 | {action: "delete", dest: "js/lock-passwordless-" + pkg.version + ".min.js"}, 37 | {action: "delete", dest: "js/lock-passwordless-" + major_version + ".js"}, 38 | {action: "delete", dest: "js/lock-passwordless-" + major_version + ".min.js"}, 39 | {action: "delete", dest: "js/lock-passwordless-" + minor_version + ".js"}, 40 | {action: "delete", dest: "js/lock-passwordless-" + minor_version + ".min.js"} 41 | ] 42 | }, 43 | publish: { 44 | files: [ 45 | { 46 | expand: true, 47 | cwd: "release/", 48 | src: ["**"], 49 | dest: "js/" 50 | } 51 | ] 52 | } 53 | }, 54 | babel: { 55 | dist: { 56 | files: [ 57 | { 58 | expand: true, 59 | cwd: "src", 60 | src: ["**/*.js", "**/*.jsx"], 61 | dest: "lib", 62 | ext: '.js' 63 | } 64 | ] 65 | } 66 | }, 67 | browserify: { 68 | options: { 69 | browserifyOptions: { 70 | extensions: ".jsx", 71 | transform: ["babelify"] 72 | } 73 | }, 74 | dev: { 75 | options: { 76 | browserifyOptions: { 77 | debug: true, 78 | extensions: ".jsx", 79 | transform: ["babelify"] 80 | }, 81 | watch: true 82 | }, 83 | src: "src/browser.js", 84 | dest: "build/lock-passwordless.js" 85 | }, 86 | build: { 87 | src: "src/browser.js", 88 | dest: "build/lock-passwordless.js" 89 | }, 90 | design: { 91 | options: { 92 | watch: true, 93 | }, 94 | src: "support/design/index.js", 95 | dest: "build/lock-passwordless.design.js" 96 | } 97 | }, 98 | clean: { 99 | build: ["build/", "release/"], 100 | dev: ["build/"], 101 | dist: ["lib/"] 102 | }, 103 | connect: { 104 | dev: { 105 | options: { 106 | hostname: "*", 107 | base: [".", "build", "support", "support/playground"], 108 | port: process.env.PORT || 3000 109 | } 110 | }, 111 | }, 112 | copy: { 113 | release: { 114 | files: [ 115 | {expand: true, flatten: true, src: "build/*", dest: "release/", rename: rename_release(pkg.version)}, 116 | {expand: true, flatten: true, src: "build/*", dest: "release/", rename: rename_release(minor_version)}, 117 | {expand: true, flatten: true, src: "build/*", dest: "release/", rename: rename_release(major_version)} 118 | ] 119 | }, 120 | pages: { 121 | files: [ 122 | {expand: true, flatten: true, src: "build/*", dest: "support/playground/build/"}, 123 | ] 124 | } 125 | }, 126 | env: { 127 | build: { 128 | NODE_ENV: "production" 129 | } 130 | }, 131 | exec: { 132 | touch_index: "touch src/index.js" 133 | }, 134 | http: { 135 | purge_js: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + pkg.version + ".js", method: "DELETE"}}, 136 | purge_js_min: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + pkg.version + ".min.js", method: "DELETE"}}, 137 | purge_major_js: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + major_version + ".js", method: "DELETE"}}, 138 | purge_major_js_min: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + major_version + ".min.js", method: "DELETE"}}, 139 | purge_minor_js: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + minor_version + ".js", method: "DELETE"}}, 140 | purge_minor_js_min: {options: {url: process.env.CDN_ROOT + "/js/lock-passwordless-" + minor_version + ".min.js", method: "DELETE"} } 141 | }, 142 | stylus: { 143 | build: { 144 | options: { 145 | compress: false // temp 146 | }, 147 | src: "css/index.styl", 148 | dest: "css/index.css" 149 | } 150 | }, 151 | watch: { 152 | stylus: { 153 | files: ["css/index.styl"], 154 | tasks: ["stylus:build", "exec:touch_index"] 155 | } 156 | }, 157 | uglify: { 158 | build: { 159 | src: "build/lock-passwordless.js", 160 | dest: "build/lock-passwordless.min.js" 161 | } 162 | } 163 | }); 164 | 165 | grunt.loadNpmTasks("grunt-aws-s3"); 166 | grunt.loadNpmTasks("grunt-babel"); 167 | grunt.loadNpmTasks("grunt-browserify"); 168 | grunt.loadNpmTasks("grunt-contrib-clean"); 169 | grunt.loadNpmTasks("grunt-contrib-connect"); 170 | grunt.loadNpmTasks("grunt-contrib-copy"); 171 | grunt.loadNpmTasks('grunt-contrib-stylus'); 172 | grunt.loadNpmTasks("grunt-contrib-uglify"); 173 | grunt.loadNpmTasks('grunt-contrib-watch'); 174 | grunt.loadNpmTasks("grunt-env"); 175 | grunt.loadNpmTasks("grunt-exec"); 176 | grunt.loadNpmTasks("grunt-http"); 177 | 178 | 179 | grunt.registerTask("build", ["clean:build", "env:build", "stylus:build", "browserify:build", "uglify:build"]); 180 | grunt.registerTask("dist", ["clean:dist", "stylus:build", "babel:dist"]); 181 | grunt.registerTask("prepare_dev", ["clean:dev", "connect:dev", "stylus:build"]); 182 | grunt.registerTask("dev", ["prepare_dev", "browserify:dev", "watch"]); 183 | grunt.registerTask("design", ["prepare_dev", "browserify:design", "watch"]); 184 | grunt.registerTask("purge_cdn", ["http:purge_js", "http:purge_js_min", "http:purge_major_js", "http:purge_major_js_min", "http:purge_minor_js", "http:purge_minor_js_min"]); 185 | grunt.registerTask("cdn", ["build", "copy:release", "aws_s3:clean", "aws_s3:publish", "purge_cdn"]); 186 | grunt.registerTask("ghpages", ["build", "copy:pages"]); // add publish task 187 | }; 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 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 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | BIN="./node_modules/.bin" 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | VERSION=$(node -e "console.log(require('$DIR/../package.json').version)") 7 | PACKAGE=$(node -e "console.log(require('$DIR/../package.json').name)") 8 | 9 | TAG_NAME="v$VERSION" 10 | TAG_EXISTS=$(git tag -l "$TAG_NAME") 11 | 12 | if [ ! -z "$TAG_EXISTS" ]; then 13 | echo "There is already a tag $TAG_EXISTS in git. Skiping git deploy." 14 | else 15 | echo "Deploying $VERSION to git" 16 | 17 | LAST_COMMIT=$(git log -1 --pretty=%B) 18 | git checkout -b dist 19 | grep -v '^build$' .gitignore > /tmp/.gitignore 20 | mv /tmp/.gitignore .gitignore 21 | git add --force build/* 22 | git commit -am "$TAG_NAME" 23 | git tag "$TAG_NAME" -m "$LAST_COMMIT" 24 | git checkout master 25 | git push origin $TAG_NAME 26 | git branch -D dist 27 | fi 28 | 29 | NPM_EXISTS=$(npm info $PACKAGE@$VERSION) 30 | 31 | if [ ! -z "$NPM_EXISTS" ]; then 32 | echo "There is already a version $VERSION in npm. Skiping npm publish." 33 | else 34 | echo "Deploying $VERSION to npm" 35 | npm publish 36 | fi 37 | 38 | CDN_EXISTS=$(curl -s -o /dev/null -w "%{http_code}" https://cdn.auth0.com/js/lock-passwordless-$VERSION.min.js | grep 200 || true) 39 | 40 | if [ ! -z "$CDN_EXISTS" ]; then 41 | echo "There is already a version $VERSION in the CDN. Skiping cdn publish." 42 | else 43 | echo "Deploying $VERSION to cdn" 44 | $BIN/grunt cdn 45 | fi 46 | 47 | echo "Preparing gh-pages deployment" 48 | $BIN/grunt ghpages 49 | 50 | echo "Publishing latest to gh-pages" 51 | git subtree split --prefix support/playground -b gh-pages 52 | git checkout gh-pages 53 | rm -rf node_modules support npm-debug.log 54 | git add . 55 | git commit -m "Automated gh-page Publish" 56 | git push -f origin gh-pages 57 | git checkout master 58 | git branch -D gh-pages 59 | -------------------------------------------------------------------------------- /bin/version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path'); 3 | var bump = require('bump-version'); 4 | var unreleased = require('unreleased'); 5 | var fs = require('fs'); 6 | 7 | function run(cmd, done) { 8 | var exec = require('child_process').exec; 9 | var proc = exec(cmd, done); 10 | proc.stdout.pipe(process.stdout); 11 | proc.stderr.pipe(process.stderr); 12 | } 13 | 14 | function replaceDistScriptNames(str, scriptName, version, bumpedVersion) { 15 | var v = version.split("."); 16 | var vb = bumpedVersion.split("."); 17 | var regExp = new RegExp(scriptName + "-" + v[0] + "(\." + v[1] + ")?(\." + v[2] + ")?(\.min)?.js", "g"); 18 | 19 | return str.replace(regExp, function (_, minor, patch, min) { 20 | var r = [scriptName + "-" + vb[0]]; 21 | if (minor) r.push(vb[1]); 22 | if (patch) r.push(vb[2]); 23 | if (min) r.push("min"); 24 | r.push("js"); 25 | return r.join("."); 26 | }); 27 | } 28 | 29 | function replaceDistScriptNamesInFile(filePath, scriptName, version, bumpedVersion) { 30 | var str = fs.readFileSync(filePath, "utf8"); 31 | var bumpedStr = replaceDistScriptNames(str, scriptName, version, bumpedVersion); 32 | fs.writeFileSync(filePath, bumpedStr); 33 | } 34 | 35 | try { 36 | var version = require('../package.json').version; 37 | var rootDir = path.resolve(__dirname, '..'); 38 | var res = bump(rootDir, process.argv.pop()); 39 | 40 | replaceDistScriptNamesInFile("README.md", "lock-passwordless", version, res.version); 41 | 42 | unreleased(res.version, rootDir, 'auth0/lock-passwordless', function (err, changelog) { 43 | if (err) { return process.exit(1); } 44 | 45 | changelog = new Buffer(changelog + '\n\n'); 46 | var file = path.join(rootDir, 'CHANGELOG.md'); 47 | 48 | var data = fs.readFileSync(file); //read existing contents into data 49 | var fd = fs.openSync(file, 'w+'); 50 | fs.writeSync(fd, changelog, 0, changelog.length); //write new data 51 | fs.writeSync(fd, data, 0, data.length); //append old data 52 | fs.close(fd); 53 | 54 | run('git commit -am "Release: ' + res.version + '"', function (err) { 55 | if (!err) { return process.exit(0); } 56 | 57 | // restore original status 58 | console.error(err.message); 59 | run('git checkout .', function (err) { 60 | if (err) { console.error(err.message); } 61 | process.exit(1); 62 | }); 63 | }); 64 | }); 65 | 66 | } catch (err) { 67 | console.error(err.message); 68 | process.exit(1); 69 | } 70 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-lock-passwordless", 3 | "version": "2.2.3", 4 | "main": "build/lock-passwordless.js", 5 | "ignore": [ 6 | "lib-cov", 7 | "*.seed", 8 | "*.log", 9 | "*.csv", 10 | "*.dat", 11 | "*.out", 12 | "*.pid", 13 | "*.gz", 14 | "pids", 15 | "logs", 16 | "results", 17 | "npm-debug.log", 18 | "bin" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /css/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url("../img/bg.png"); 3 | background-size: cover; 4 | min-height: 800px; 5 | } 6 | .demo-button { 7 | background: #fff; 8 | border-radius: 3px; 9 | padding: 10px 20px; 10 | color: #000; 11 | text-transform: uppercase; 12 | font-size: 11px; 13 | font-weight: 600; 14 | letter-spacing: 1px; 15 | } 16 | .js-hide { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /css/demo.styl: -------------------------------------------------------------------------------- 1 | body 2 | background: url('../img/bg.png') 3 | background-size: cover; 4 | min-height: 800px; 5 | 6 | .demo-button 7 | background: white; 8 | border-radius: 3px; 9 | padding: 10px 20px; 10 | color: black; 11 | text-transform: uppercase; 12 | font-size: 11px; 13 | font-weight: 600; 14 | letter-spacing: 1px; 15 | 16 | .js-hide 17 | display: none; 18 | -------------------------------------------------------------------------------- /css/normalize.styl: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /css/transitions.css: -------------------------------------------------------------------------------- 1 | 2 | /*Slide in*/ 3 | 4 | .slide-enter { 5 | -webkit-transform: translate3d(100%, 0, 0); 6 | transform: translate3d(100%, 0, 0); 7 | } 8 | 9 | .slide-enter.slide-enter-active { 10 | -webkit-transform: translate3d(0, 0, 0); 11 | transform: translate3d(0, 0, 0); 12 | -webkit-transition: -webkit-transform .3s ease-out; 13 | transition: transform .3s ease-out; 14 | } 15 | 16 | /*Slide out*/ 17 | 18 | .slide-leave { 19 | -webkit-transform: translate3d(0, 0, 0); 20 | transform: translate3d(0, 0, 0); 21 | } 22 | 23 | .slide-leave.slide-leave-active { 24 | -webkit-transform: translate3d(100%, 0, 0); 25 | transform: translate3d(100%, 0, 0); 26 | -webkit-transition: -webkit-transform .3s ease-in; 27 | transition: transform .3s ease-in; 28 | } 29 | 30 | /*Horizontal fade in*/ 31 | 32 | .horizontal-fade-enter { 33 | position: relative; 34 | top: 0; 35 | left: 0; 36 | z-index: 1; 37 | 38 | -webkit-transform: translate3d(100%, 0, 0); 39 | transform: translate3d(100%, 0, 0); 40 | } 41 | 42 | .horizontal-fade-enter.horizontal-fade-enter-active { 43 | -webkit-transform: none; 44 | transform: none; 45 | 46 | -webkit-transition: -webkit-transform linear; 47 | transition: transform linear; 48 | } 49 | 50 | /*Horizontal fade out*/ 51 | 52 | .horizontal-fade-leave { 53 | opacity: 1; 54 | position: absolute; 55 | width: 100%; 56 | top: 0; 57 | left: 0; 58 | } 59 | 60 | .horizontal-fade-leave.horizontal-fade-leave-active { 61 | opacity: 0; 62 | -webkit-transform: translate3d(-100%, 0, 0); 63 | transform: translate3d(-100%, 0, 0); 64 | 65 | -webkit-transition: -webkit-transform .5s, opacity 1s linear; 66 | transition: transform .5s, opacity 1s linear; 67 | } 68 | 69 | /* reverse fade in */ 70 | 71 | .reverse-horizontal-fade-enter { 72 | position: relative; 73 | top: 0; 74 | left: 0; 75 | z-index: 1; 76 | 77 | -webkit-transform: translate3d(-100%, 0, 0); 78 | transform: translate3d(-100%, 0, 0); 79 | } 80 | 81 | .reverse-horizontal-fade-enter.reverse-horizontal-fade-enter-active { 82 | -webkit-transform: none; 83 | transform: none; 84 | 85 | -webkit-transition: -webkit-transform linear; 86 | transition: transform linear; 87 | } 88 | 89 | /* reverse fade out */ 90 | 91 | .reverse-horizontal-fade-leave { 92 | opacity: 1; 93 | position: absolute; 94 | width: 100%; 95 | top: 0; 96 | left: 0; 97 | } 98 | 99 | .reverse-horizontal-fade-leave.reverse-horizontal-fade-leave-active { 100 | opacity: 0; 101 | -webkit-transform: translate3d(100%, 0, 0); 102 | transform: translate3d(100%, 0, 0); 103 | 104 | -webkit-transition: -webkit-transform .5s, opacity 1s linear; 105 | transition: transform .5s, opacity 1s linear; 106 | } 107 | -------------------------------------------------------------------------------- /examples/bundling/browserify/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /examples/bundling/browserify/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build.js 3 | -------------------------------------------------------------------------------- /examples/bundling/browserify/README.md: -------------------------------------------------------------------------------- 1 | # Browserify example 2 | 3 | This example shows how to bundle the `auth0-lock-passwordless` npm package with [browserify](http://browserify.org). It uses [Babel](https://babeljs.io/) to support the new [ES2015 syntax](https://babeljs.io/docs/learn-es2015/), but it is not a requirement. 4 | 5 | First, ensure you have [node](https://nodejs.org/) installed on your system. Then, run `npm install` to install the project's dependencies and `npm run build` to build the bundle. Finally, open `index.html` in your favorite browser. 6 | -------------------------------------------------------------------------------- /examples/bundling/browserify/index.html: -------------------------------------------------------------------------------- 1 | 2 | Browserify Bundle 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/bundling/browserify/index.js: -------------------------------------------------------------------------------- 1 | import Auth0LockPasswordless from 'auth0-lock-passwordless'; 2 | 3 | const cid = "Gcs1jSu5FAFpDrPe0jrqGpfbTEkzCk15"; 4 | const domain = "pwdlessdemo.auth0.com"; 5 | 6 | const lock = new Auth0LockPasswordless(cid, domain); 7 | 8 | lock.magiclink(); 9 | -------------------------------------------------------------------------------- /examples/bundling/browserify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-lock-passwordless-browserify", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify index.js -o build.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "browserify": { 13 | "transform": [ 14 | "babelify" 15 | ] 16 | }, 17 | "dependencies": { 18 | "auth0-lock-passwordless": "2.2.3" 19 | }, 20 | "devDependencies": { 21 | "babel-preset-es2015": "^6.3.13", 22 | "babelify": "^7.2.0", 23 | "browserify": "^12.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/bundling/webpack/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /examples/bundling/webpack/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build.js 3 | -------------------------------------------------------------------------------- /examples/bundling/webpack/README.md: -------------------------------------------------------------------------------- 1 | # Webpack example 2 | 3 | This example shows how to bundle the `auth0-lock-passwordless` npm package with [webpack](http://webpack.github.io/). It uses [Babel](https://babeljs.io/) to support the new [ES2015 syntax](https://babeljs.io/docs/learn-es2015/), but it is not a requirement. 4 | 5 | First, ensure you have [node](https://nodejs.org/) installed on your system. Then, run `npm install` to install the project's dependencies and `npm run build` to build the bundle. Finally, open `index.html` in your favorite browser. 6 | -------------------------------------------------------------------------------- /examples/bundling/webpack/index.html: -------------------------------------------------------------------------------- 1 | 2 | Webpack Bundle 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/bundling/webpack/index.js: -------------------------------------------------------------------------------- 1 | import Auth0LockPasswordless from 'auth0-lock-passwordless'; 2 | 3 | const cid = "Gcs1jSu5FAFpDrPe0jrqGpfbTEkzCk15"; 4 | const domain = "pwdlessdemo.auth0.com"; 5 | 6 | const lock = new Auth0LockPasswordless(cid, domain); 7 | 8 | lock.magiclink(); 9 | -------------------------------------------------------------------------------- /examples/bundling/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-lock-passwordless-webpack", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "auth0-lock-passwordless": "2.2.3" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.4.0", 17 | "babel-loader": "^6.2.1", 18 | "babel-preset-es2015": "^6.3.13", 19 | "webpack": "^1.12.9" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/bundling/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./index.js", 3 | output: { 4 | filename: 'build.js' 5 | }, 6 | module: { 7 | loaders: [{ 8 | test: /\.js$/, 9 | exclude: /(node_modules|bower_components)/, 10 | loader: 'babel' 11 | }] 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-lock-passwordless", 3 | "version": "2.2.3", 4 | "description": "Auth0 Lock Passwordless", 5 | "author": "Auth0 (http://auth0.com)", 6 | "license": "MIT", 7 | "keywords": [ 8 | "auth0", 9 | "auth", 10 | "openid", 11 | "authentication", 12 | "passwordless", 13 | "browser", 14 | "jwt" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/auth0/lock-passwordless" 19 | }, 20 | "main": "lib/index.js", 21 | "scripts": { 22 | "start": "grunt dev", 23 | "deploy": "./bin/deploy", 24 | "build": "grunt build", 25 | "design": "grunt design", 26 | "dist": "grunt dist", 27 | "prepublish": "grunt dist", 28 | "postinstall": "echo \"WARNING: Version 2.2.0 introduces changes to the bundling process, see CHANGELOG.md.\"", 29 | "test": "zuul -- test/*/*.test.js", 30 | "test:browser": "zuul --local 8080 --disable-tunnel -- test/*/*.test.js", 31 | "test:phantom": "mochify --recursive --extension=.jsx" 32 | }, 33 | "devDependencies": { 34 | "babel-plugin-transform-css-import-to-string": "0.0.2", 35 | "babel-plugin-version-inline": "^1.0.0", 36 | "babel-preset-es2015": "^6.3.13", 37 | "babel-preset-es2015-loose": "^7.0.0", 38 | "babel-preset-react": "^6.3.13", 39 | "babel-preset-stage-0": "^6.3.13", 40 | "babelify": "^7.2.0", 41 | "browserify": "^12.0.1", 42 | "bump-version": "^0.5.0", 43 | "expect.js": "^0.3.1", 44 | "grunt": "^0.4.5", 45 | "grunt-aws-s3": "^0.14.0", 46 | "grunt-babel": "^6.0.0", 47 | "grunt-browserify": "^4.0.0", 48 | "grunt-cli": "^0.1.13", 49 | "grunt-contrib-clean": "^0.6.0", 50 | "grunt-contrib-connect": "^0.11.2", 51 | "grunt-contrib-copy": "^0.8.0", 52 | "grunt-contrib-stylus": "^0.22.0", 53 | "grunt-contrib-uglify": "^0.9.1", 54 | "grunt-contrib-watch": "^0.6.1", 55 | "grunt-env": "^0.4.4", 56 | "grunt-exec": "^0.4.6", 57 | "grunt-http": "^1.6.0", 58 | "mochify": "^2.12.0", 59 | "react-addons-test-utils": "^0.14.0", 60 | "sinon": "^1.15.4", 61 | "unreleased": "^0.1.0", 62 | "zuul": "3.10.1", 63 | "zuul-ngrok": "gnandretta/zuul-ngrok#upgrade-ngrok" 64 | }, 65 | "dependencies": { 66 | "auth0-js": "6.8.0", 67 | "blueimp-md5": "^1.1.0", 68 | "fbjs": "^0.3.1", 69 | "immutable": "^3.7.3", 70 | "jsonp": "^0.2.0", 71 | "react": "^0.14.0", 72 | "react-addons-css-transition-group": "^0.14.0", 73 | "react-addons-transition-group": "^0.14.0", 74 | "react-dom": "^0.14.0", 75 | "reqwest": "^1.1.4", 76 | "trim": "0.0.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/atom/index.js: -------------------------------------------------------------------------------- 1 | // TODO: Hack to preload the default icon. We need to preload also custom icons 2 | // and prevent showing the lock until they are loaded. 3 | const img = document.createElement("img"); 4 | img.src = "//cdn.auth0.com/styleguide/4.6.1/lib/logos/img/badge.png"; 5 | 6 | class Atom { 7 | constructor(state) { 8 | this.state = state; 9 | this.watches = {}; 10 | } 11 | 12 | reset(state) { 13 | return this._change(state); 14 | } 15 | 16 | swap(f, ...args) { 17 | return this._change(f(this.state, ...args)); 18 | } 19 | 20 | deref() { 21 | return this.state; 22 | } 23 | 24 | addWatch(k, f) { 25 | // if (this.watches[key]) { 26 | // console.warn(`adding a watch with an already registered key: ${k}`); 27 | // } 28 | this.watches[k] = f; 29 | return this; 30 | } 31 | 32 | removeWatch(k) { 33 | // if (!this.watches[key]) { 34 | // console.warn(`removing a watch with an unknown key: ${k}`); 35 | // } 36 | delete this.watches[k]; 37 | return this; 38 | } 39 | 40 | _change(newState) { 41 | const { state, watches } = this; 42 | this.state = newState; 43 | Object.keys(watches).forEach(k => watches[k](k, state, newState)); 44 | return this.state; 45 | } 46 | } 47 | 48 | export default function atom(state) { 49 | return new Atom(state); 50 | } 51 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * This is used to build the bundle with browserify. 4 | * 5 | * The bundle is used by people who doesn't use browserify. 6 | * Those who use browserify will install with npm and require the module, 7 | * the package.json file points to index.js. 8 | */ 9 | import Auth0LockPasswordless from './index'; 10 | 11 | //use amd or just throught to window object. 12 | if (typeof global.window.define == 'function' && global.window.define.amd) { 13 | global.window.define('auth0LockPasswordless', function () { return Auth0LockPasswordless; }); 14 | } else if (global.window) { 15 | global.window.Auth0LockPasswordless = Auth0LockPasswordless; 16 | } 17 | -------------------------------------------------------------------------------- /src/cred/email/actions.js: -------------------------------------------------------------------------------- 1 | import { swap, updateEntity } from '../../store/index'; 2 | import * as c from '../index'; 3 | 4 | export function changeEmail(id, email) { 5 | swap(updateEntity, "lock", id, c.setEmail, email); 6 | } 7 | -------------------------------------------------------------------------------- /src/cred/email/ask_email.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screen from '../../lock/screen'; 3 | import EmailPane from './email_pane'; 4 | 5 | 6 | export default class AskEmail extends Screen { 7 | 8 | constructor() { 9 | super("email"); 10 | } 11 | 12 | render({lock}) { 13 | return ( 14 | 18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/cred/email/email_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InputWrap from '../input_wrap'; 3 | import Icon from '../../icon/icon'; 4 | import { debouncedRequestGravatar, requestGravatar } from '../../gravatar/actions'; 5 | 6 | export default class EmailInput extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | componentDidMount() { 13 | const email = this.refs.input.value; 14 | if (email && this.props.gravatar) { 15 | requestGravatar(email); 16 | } 17 | } 18 | 19 | render() { 20 | const { isValid, onChange, gravatar, ...props } = this.props; 21 | const { focused } = this.state; 22 | 23 | return ( 24 | } focused={focused}> 25 | 36 | 37 | ); 38 | } 39 | 40 | handleOnChange(e) { 41 | if (this.props.gravatar) { 42 | debouncedRequestGravatar(e.target.value); 43 | } 44 | 45 | if (this.props.onChange) { 46 | this.props.onChange(e); 47 | } 48 | } 49 | 50 | handleFocus() { 51 | this.setState({focused: true}); 52 | } 53 | 54 | handleBlur() { 55 | this.setState({focused: false}); 56 | } 57 | } 58 | 59 | // TODO: specify propTypes 60 | -------------------------------------------------------------------------------- /src/cred/email/email_pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EmailInput from './email_input'; 3 | import * as c from '../index'; 4 | import { changeEmail } from './actions'; 5 | import * as l from '../../lock/index'; 6 | 7 | export default class EmailPane extends React.Component { 8 | 9 | handleChange(e) { 10 | changeEmail(l.id(this.props.lock), e.target.value); 11 | } 12 | 13 | render() { 14 | const { lock, placeholder, tabIndex } = this.props; 15 | 16 | return ( 17 | 25 | ); 26 | } 27 | 28 | } 29 | 30 | EmailPane.propTypes = { 31 | lock: React.PropTypes.object.isRequired, 32 | placeholder: React.PropTypes.string.isRequired, 33 | tabIndex: React.PropTypes.number.isRequired 34 | }; 35 | 36 | EmailPane.defaultProps = { 37 | tabIndex: 1 38 | }; 39 | -------------------------------------------------------------------------------- /src/cred/index.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import trim from 'trim'; 3 | import * as cc from './country_codes'; 4 | 5 | 6 | function valid(lock, field) { 7 | return lock.getIn(["cred", field, "valid"]); 8 | } 9 | 10 | function showInvalid(lock, cred) { 11 | return lock.getIn(["cred", cred, "showInvalid"], false); 12 | } 13 | 14 | function setShowInvalid(lock, cred, value) { 15 | return lock.setIn(["cred", cred, "showInvalid"], value); 16 | } 17 | 18 | function visiblyInvalid(lock, cred) { 19 | return showInvalid(lock, cred) && !valid(lock, cred); 20 | } 21 | 22 | // phone number 23 | 24 | export function fullPhoneNumber(lock) { 25 | return `${phoneDialingCode(lock) || ""}${phoneNumber(lock) || ""}`.replace(/[\s-]+/g, ''); 26 | } 27 | 28 | export function fullHumanPhoneNumber(m) { 29 | const code = phoneDialingCode(m); 30 | const number = phoneNumber(m); 31 | return `${code} ${number}`; 32 | } 33 | 34 | export function setPhoneLocation(m, value) { 35 | return m.setIn(["cred", "phoneNumber", "location"], value); 36 | } 37 | 38 | function phoneLocation(m) { 39 | return m.getIn(["cred", "phoneNumber", "location"], cc.defaultLocation); 40 | } 41 | 42 | export function phoneLocationString(m) { 43 | return cc.locationString(phoneLocation(m)); 44 | } 45 | 46 | export function phoneDialingCode(m) { 47 | return cc.dialingCode(phoneLocation(m)); 48 | } 49 | 50 | export function phoneIsoCode(m) { 51 | return cc.isoCode(phoneLocation(m)); 52 | } 53 | 54 | export function phoneNumber(lock) { 55 | return lock.getIn(["cred", "phoneNumber", "number"], ""); 56 | } 57 | 58 | export function setPhoneNumber(lock, value) { 59 | const prevValue = phoneNumber(lock); 60 | const prevShowInvalid = showInvalid(lock, "phoneNumber"); 61 | const valid = validatePhoneNumber(value); 62 | 63 | return lock.mergeIn(["cred", "phoneNumber"], Map({ 64 | number: value, 65 | valid: valid, 66 | showInvalid: prevShowInvalid && prevValue === value 67 | })); 68 | } 69 | 70 | export function validatePhoneNumber(phoneNumber) { 71 | const regExp = /^[0-9]([0-9 -])*[0-9]$/; 72 | return regExp.test(phoneNumber); 73 | } 74 | 75 | export function validPhoneNumber(lock) { 76 | return valid(lock, "phoneNumber"); 77 | } 78 | 79 | export function visiblyInvalidPhoneNumber(lock) { 80 | return visiblyInvalid(lock, "phoneNumber"); 81 | } 82 | 83 | export function setShowInvalidPhoneNumber(lock, value) { 84 | return setShowInvalid(lock, "phoneNumber", value); 85 | } 86 | 87 | // email 88 | 89 | export function email(lock) { 90 | return lock.getIn(["cred", "email", "email"], ""); 91 | } 92 | 93 | export function setEmail(lock, value) { 94 | const prevValue = email(lock); 95 | const prevShowInvalid = showInvalid(lock, "email"); 96 | const valid = !!validateEmail(value); 97 | 98 | return lock.mergeIn(["cred", "email"], Map({ 99 | email: value, 100 | valid: valid, 101 | showInvalid: prevShowInvalid && prevValue === value 102 | })); 103 | } 104 | 105 | export function validateEmail(email) { 106 | const regExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 107 | const result = regExp.exec(trim(email.toLowerCase())); 108 | return result && result[0]; 109 | } 110 | 111 | export function validEmail(lock) { 112 | return valid(lock, "email"); 113 | } 114 | 115 | export function visiblyInvalidEmail(lock) { 116 | return visiblyInvalid(lock, "email"); 117 | } 118 | 119 | export function setShowInvalidEmail(lock, value = true) { 120 | return setShowInvalid(lock, "email", value); 121 | } 122 | 123 | // vcode 124 | 125 | export function vcode(lock) { 126 | return lock.getIn(["cred", "vcode", "vcode"], ""); 127 | } 128 | 129 | export function setVcode(lock, value) { 130 | const prevValue = vcode(lock); 131 | const prevShowInvalid = showInvalid(lock, "vcode"); 132 | const valid = validateVcode(value); 133 | 134 | return lock.mergeIn(["cred", "vcode"], Map({ 135 | vcode: value, 136 | valid: valid, 137 | showInvalid: prevShowInvalid && prevValue === value 138 | })); 139 | } 140 | 141 | export function validateVcode(vcode) { 142 | return trim(vcode).length > 0; 143 | } 144 | 145 | export function validVcode(lock) { 146 | return valid(lock, "vcode"); 147 | } 148 | 149 | export function visiblyInvalidVcode(lock) { 150 | return visiblyInvalid(lock, "vcode"); 151 | } 152 | 153 | export function setShowInvalidVcode(lock, value = true) { 154 | return setShowInvalid(lock, "vcode", value); 155 | } 156 | -------------------------------------------------------------------------------- /src/cred/input_wrap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export default class InputWrap extends React.Component { 5 | render() { 6 | const { focused, icon, isValid } = this.props; 7 | let blockClassName = "auth0-lock-input-block auth0-lock-input-" + this.props.name; 8 | if (!isValid) { 9 | blockClassName += " auth0-lock-error animated pulse"; 10 | } 11 | 12 | let wrapClassName = "auth0-lock-input-wrap"; 13 | if (focused && isValid) { 14 | wrapClassName += " auth0-lock-focused"; 15 | } 16 | 17 | const fallbackIcon = ; 18 | 19 | return ( 20 |
21 |
22 | {icon || fallbackIcon} 23 | {this.props.children} 24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | InputWrap.propTypes = { 31 | name: React.PropTypes.string.isRequired, 32 | isValid: React.PropTypes.bool.isRequired, 33 | children: React.PropTypes.oneOfType([ 34 | React.PropTypes.element.isRequired, 35 | React.PropTypes.arrayOf(React.PropTypes.element).isRequired 36 | ]) 37 | }; 38 | -------------------------------------------------------------------------------- /src/cred/or/ask_social_network_or_email.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screen from '../../lock/screen'; 3 | import EmailPane from '../email/email_pane'; 4 | import SocialButtonsPane from '../social/social_buttons_pane'; 5 | import PaneSeparator from '../../lock/pane_separator'; 6 | 7 | import { requestPasswordlessEmail } from '../../passwordless/actions'; 8 | import { renderEmailSentConfirmation } from '../../passwordless/email_sent_confirmation'; 9 | import { renderSignedInConfirmation } from '../../lock/signed_in_confirmation'; 10 | 11 | export default class AskSocialNetworkOrEmail extends Screen { 12 | 13 | constructor() { 14 | super("networkOrEmail"); 15 | } 16 | 17 | submitHandler() { 18 | return requestPasswordlessEmail; 19 | } 20 | 21 | renderAuxiliaryPane(lock) { 22 | return renderEmailSentConfirmation(lock) 23 | || renderSignedInConfirmation(lock); 24 | } 25 | 26 | render({lock}) { 27 | return ( 28 |
29 | 33 | {this.t(lock, ["separatorText"])} 34 | 39 |
40 | ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/cred/or/ask_social_network_or_phone_number.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Base from '../../passwordless/ask_phone_number'; 3 | import PhoneNumberPane from '../phone-number/phone_number_pane'; 4 | import SocialButtonsPane from '../social/social_buttons_pane'; 5 | import PaneSeparator from '../../lock/pane_separator'; 6 | import * as l from '../../lock/index'; 7 | 8 | import { renderSignedInConfirmation } from '../../lock/signed_in_confirmation'; 9 | 10 | export default class AskSocialNetworkOrPhoneNumber extends Base { 11 | 12 | constructor() { 13 | super(); 14 | this.name = "networkOrPhone"; 15 | } 16 | 17 | renderAuxiliaryPane(lock) { 18 | return renderSignedInConfirmation(lock) || super.renderAuxiliaryPane(lock); 19 | } 20 | 21 | render({focusSubmit, lock}) { 22 | return ( 23 |
24 | 28 | {this.t(lock, ["separatorText"])} 29 | 35 |
36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/cred/phone-number/actions.js: -------------------------------------------------------------------------------- 1 | import { getEntity, read, swap, updateEntity } from '../../store/index'; 2 | import * as c from '../index'; 3 | import * as cc from '../country_codes'; 4 | import { 5 | closeLocationSelect, 6 | openLocationSelect 7 | } from './index'; 8 | 9 | export function changePhoneNumber(id, phoneNumber) { 10 | swap(updateEntity, "lock", id, c.setPhoneNumber, phoneNumber); 11 | } 12 | 13 | export function changePhoneLocation(id, location) { 14 | swap(updateEntity, "lock", id, lock => { 15 | lock = closeLocationSelect(lock); 16 | lock = c.setPhoneLocation(lock, location); 17 | return lock; 18 | }); 19 | } 20 | 21 | // TODO: move this to another place since is not really an action. 22 | export function setInitialPhoneLocation(m, options) { 23 | const { defaultLocation } = options; 24 | 25 | if (defaultLocation && typeof defaultLocation === "string") { 26 | const location = cc.findByIsoCode(defaultLocation); 27 | if (!location) { 28 | throw new Error(`Unable to set the default location, can't find any country with the code "${defaultLocation}".`); 29 | } 30 | return c.setPhoneLocation(m, location); 31 | } else { 32 | const user = read(getEntity, "user"); 33 | const location = cc.findByIsoCode(user && user.get("location")); 34 | return location ? c.setPhoneLocation(m, location) : m; 35 | } 36 | } 37 | 38 | export function selectPhoneLocation(id, searchStr) { 39 | swap(updateEntity, "lock", id, openLocationSelect, searchStr); 40 | } 41 | 42 | export function cancelSelectPhoneLocation(id) { 43 | swap(updateEntity, "lock", id, closeLocationSelect); 44 | } 45 | -------------------------------------------------------------------------------- /src/cred/phone-number/ask_location.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LocationSelect from './location_select'; 3 | import { cancelSelectPhoneLocation, changePhoneLocation } from './actions'; 4 | import { initialLocationSearchStr, selectingLocation } from './index'; 5 | import * as l from '../../lock/index'; 6 | 7 | export default class AskLocation extends React.Component { 8 | 9 | handleCancel() { 10 | cancelSelectPhoneLocation(l.id(this.props.lock)); 11 | } 12 | 13 | handleSelect(location) { 14 | changePhoneLocation(l.id(this.props.lock), location); 15 | } 16 | 17 | t(keyPath, params) { 18 | return l.ui.t(this.props.lock, ["location"].concat(keyPath), params); 19 | } 20 | 21 | render() { 22 | return ( 23 | 29 | ); 30 | } 31 | 32 | } 33 | 34 | export function renderAskLocation(lock) { 35 | return selectingLocation(lock) 36 | ? 40 | : null; 41 | } 42 | -------------------------------------------------------------------------------- /src/cred/phone-number/ask_phone_number.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screen from '../../lock/screen'; 3 | import PhoneNumberPane from './phone_number_pane'; 4 | 5 | import { renderAskLocation } from './ask_location'; 6 | 7 | import { cancelSelectPhoneLocation } from './actions'; 8 | import { selectingLocation } from './index'; 9 | 10 | 11 | export default class AskPhoneNumber extends Screen { 12 | 13 | constructor() { 14 | super("phone"); 15 | } 16 | 17 | escHandler(lock) { 18 | return selectingLocation(lock) ? cancelSelectPhoneLocation : null; 19 | } 20 | 21 | renderAuxiliaryPane(lock) { 22 | return renderAskLocation(lock); 23 | } 24 | 25 | render({focusSubmit, lock}) { 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/cred/phone-number/index.js: -------------------------------------------------------------------------------- 1 | export function openLocationSelect(m, searchStr) { 2 | m = m.set("selectingLocation", true); 3 | if (searchStr && typeof searchStr === "string") { 4 | m = m.set("initialLocationSearchStr", searchStr); 5 | } 6 | 7 | return m; 8 | } 9 | 10 | export function closeLocationSelect(m) { 11 | m = m.remove("selectingLocation"); 12 | m = m.remove("initialLocationSearchStr"); 13 | 14 | return m; 15 | } 16 | 17 | export function initialLocationSearchStr(m) { 18 | return m.get("initialLocationSearchStr", ""); 19 | } 20 | 21 | export function selectingLocation(m) { 22 | return m.get("selectingLocation", false); 23 | } 24 | -------------------------------------------------------------------------------- /src/cred/phone-number/location_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InputWrap from '../input_wrap'; 3 | import Icon from '../../icon/icon'; 4 | 5 | export default class LocationInput extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | const { onClick, tabIndex, value } = this.props; 13 | const { focused } = this.state; 14 | 15 | const limitedValue = value.length > 23 ? 16 | `${value.substr(0,20)}...` : value; 17 | 18 | return ( 19 | } focused={focused}> 20 | 29 | 30 | 31 | ); 32 | } 33 | 34 | handleFocus() { 35 | this.setState({focused: true}); 36 | } 37 | 38 | handleBlur() { 39 | this.setState({focused: false}); 40 | } 41 | 42 | handleKeyDown(e) { 43 | if (e.key !== "Tab") { 44 | e.preventDefault(); 45 | } 46 | 47 | if (e.key === "ArrowDown") { 48 | return this.props.onClick(); 49 | } 50 | 51 | if (e.keyCode >= 65 && e.keyCode <= 90) { 52 | return this.props.onClick(String.fromCharCode(e.keyCode).toLowerCase()); 53 | } 54 | } 55 | } 56 | 57 | // TODO: specify propTypes 58 | -------------------------------------------------------------------------------- /src/cred/phone-number/location_select.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Icon from '../../icon/icon'; 4 | import IconButton from '../../icon/button'; 5 | import * as cc from '../country_codes'; 6 | import * as su from '../../utils/string_utils'; 7 | import { isSmallScreen } from '../../utils/media_utils'; 8 | 9 | function cycle(xs, x) { 10 | const next = xs.skipWhile(y => y !== x).get(1); 11 | return next || xs.get(0); 12 | } 13 | 14 | export default class LocationSelect extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = {filteredCountryCodes: cc.countryCodes, highlighted: null}; 18 | if (props.initialLocationSearchStr) { 19 | this.state = this.filter(props.initialLocationSearchStr); 20 | } 21 | } 22 | 23 | componentDidMount() { 24 | if (!isSmallScreen()) { 25 | setTimeout(() => { 26 | const node = this.refs.input; 27 | node.focus(); 28 | if (node.setSelectionRange) { 29 | const length = node.value.length; 30 | node.setSelectionRange(length, length); 31 | } else { 32 | node.value = node.value; 33 | } 34 | }, 300); 35 | } 36 | } 37 | 38 | render() { 39 | const { 40 | initialLocationSearchStr, 41 | locationFilterInputPlaceholder, 42 | selectHandler 43 | } = this.props; 44 | 45 | const { filteredCountryCodes, highlighted } = this.state; 46 | 47 | return ( 48 |
49 |
50 | 51 |
52 | 53 | 60 |
61 |
62 | 66 |
67 | ); 68 | } 69 | 70 | filter(str) { 71 | const findNewHighlighted = (countryCodes, highlighted) => { 72 | if (countryCodes.size === 1) { 73 | return countryCodes.get(0); 74 | } 75 | 76 | return countryCodes.includes(highlighted) ? highlighted : null; 77 | } 78 | 79 | const filteredCountryCodes = cc.find(str); 80 | 81 | const { highlighted } = this.state; 82 | const newHighlighted = findNewHighlighted(filteredCountryCodes, highlighted); 83 | 84 | return { 85 | filteredCountryCodes: filteredCountryCodes, 86 | highlighted: newHighlighted 87 | }; 88 | } 89 | 90 | handleSearchChange(e) { 91 | this.setState(this.filter(e.target.value)); 92 | } 93 | 94 | handleHighlight(location) { 95 | this.setState({highlighted: location}); 96 | } 97 | 98 | highlightPrev() { 99 | const { filteredCountryCodes, highlighted } = this.state; 100 | this.setState({highlighted: cycle(filteredCountryCodes.reverse(), highlighted)}); 101 | } 102 | 103 | highlightNext() { 104 | const { filteredCountryCodes, highlighted } = this.state; 105 | this.setState({highlighted: cycle(filteredCountryCodes, highlighted)}); 106 | } 107 | 108 | selectHighlighted() { 109 | const { highlighted } = this.state; 110 | if (highlighted) { 111 | this.props.selectHandler(highlighted); 112 | } 113 | } 114 | 115 | cancel() { 116 | this.props.cancelHandler(); 117 | } 118 | 119 | handleKeyDown(e) { 120 | switch(e.key) { 121 | case "ArrowDown": 122 | e.preventDefault(); 123 | this.highlightNext(); 124 | break; 125 | case "ArrowUp": 126 | e.preventDefault(); 127 | this.highlightPrev(); 128 | break; 129 | case "Enter": 130 | e.preventDefault(); 131 | this.selectHighlighted(); 132 | break; 133 | case "Escape": 134 | e.preventDefault(); 135 | this.cancel(); 136 | default: 137 | // no-op 138 | } 139 | } 140 | } 141 | 142 | class LocationList extends React.Component { 143 | componentDidUpdate() { 144 | // NOTE: I've spent very little time on this. It works, but it surely can be 145 | // expressed more clearly. 146 | const { highlighted } = this.refs; 147 | if (highlighted) { 148 | const scrollableNode = ReactDOM.findDOMNode(this); 149 | const highlightedNode = ReactDOM.findDOMNode(highlighted); 150 | const relativeOffsetTop = highlightedNode.offsetTop - scrollableNode.scrollTop; 151 | let scrollTopDelta = 0; 152 | if (relativeOffsetTop + highlightedNode.offsetHeight > scrollableNode.clientHeight) { 153 | scrollTopDelta = relativeOffsetTop + highlightedNode.offsetHeight - scrollableNode.clientHeight; 154 | } else if (relativeOffsetTop < 0) { 155 | scrollTopDelta = relativeOffsetTop; 156 | } 157 | 158 | if (scrollTopDelta) { 159 | this.preventHighlight = true; 160 | scrollableNode.scrollTop += scrollTopDelta; 161 | if (this.timeout) { 162 | clearTimeout(this.timeout); 163 | } 164 | this.timeout = setTimeout(() => this.preventHighlight = false, 100); 165 | } 166 | } 167 | } 168 | 169 | render() { 170 | const { countryCodes, highlighted, highlightHandler, selectHandler } = this.props; 171 | 172 | const items = countryCodes.map(x => { 173 | const props = { 174 | location: x, 175 | key: cc.locationString(x).replace(/ /g, '-'), 176 | highlightHandler: ::this.handleHighlight, 177 | selectHandler: selectHandler 178 | }; 179 | 180 | if (highlighted === x) { 181 | props.highlighted = true; 182 | props.ref = "highlighted"; 183 | } 184 | 185 | return 186 | }); 187 | 188 | return ( 189 |
190 |
    {items}
191 |
192 | ); 193 | } 194 | 195 | handleHighlight(location) { 196 | // TODO: This is an ugly hack to avoid highlighting the element under the 197 | // mouse when an arrow key trigger a scroll of the list (which in turn 198 | // triggers a mousemove event). 199 | !this.preventHighlight && this.props.highlightHandler(location); 200 | } 201 | 202 | handleMouseLeave() { 203 | this.props.highlightHandler(null); 204 | } 205 | } 206 | 207 | class LocationListItem extends React.Component { 208 | shouldComponentUpdate(nextProps, nextState) { 209 | return this.props.highlighted != nextProps.highlighted; 210 | } 211 | 212 | render() { 213 | const { highlighted, location } = this.props; 214 | const className = highlighted ? "auth0-lock-list-code-highlighted" : ""; 215 | 216 | return ( 217 |
  • 220 | {`${cc.dialingCode(location)} ${cc.isoCode(location)} ${cc.country(location)}`} 221 |
  • 222 | ); 223 | } 224 | 225 | handleClick(e) { 226 | e.preventDefault(); 227 | const { location, selectHandler } = this.props; 228 | selectHandler(location); 229 | } 230 | 231 | handleMouseMove(e) { 232 | const { location, highlightHandler } = this.props; 233 | highlightHandler(location); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/cred/phone-number/phone_number_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InputWrap from '../input_wrap'; 3 | import Icon from '../../icon/icon'; 4 | 5 | export default class PhoneNumberInput extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | render() { 12 | const { isValid, ...props } = this.props; 13 | const { focused } = this.state; 14 | 15 | return ( 16 | } focused={focused}> 17 | 25 | 26 | ); 27 | } 28 | 29 | focus() { 30 | this.refs.input.focus(); 31 | this.handleFocus(); 32 | } 33 | 34 | handleFocus() { 35 | this.setState({focused: true}); 36 | } 37 | 38 | handleBlur() { 39 | this.setState({focused: false}); 40 | } 41 | } 42 | 43 | // TODO: specify propTypes 44 | -------------------------------------------------------------------------------- /src/cred/phone-number/phone_number_pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PhoneNumberInput from './phone_number_input'; 3 | import LocationInput from './location_input'; 4 | import * as c from '../index'; 5 | import * as l from '../../lock/index'; 6 | import { changePhoneNumber, selectPhoneLocation } from './actions'; 7 | import { selectingLocation } from './index'; 8 | 9 | export default class PhoneNumberPane extends React.Component { 10 | 11 | componentWillReceiveProps(nextProps) { 12 | if (selectingLocation(this.props.lock) && !selectingLocation(nextProps.lock)) { 13 | setTimeout(() => { 14 | if (c.phoneNumber(nextProps.lock)) { 15 | this.props.focusSubmit(); 16 | } else { 17 | this.focusPhoneNumberInput(); 18 | } 19 | }, 17); 20 | } 21 | } 22 | 23 | focusPhoneNumberInput() { 24 | this.refs.phoneNumberInput.focus(); 25 | } 26 | 27 | handlePhoneNumberChange(e) { 28 | changePhoneNumber(l.id(this.props.lock), e.target.value); 29 | } 30 | 31 | handleLocationClick(searchStr) { 32 | selectPhoneLocation(l.id(this.props.lock), searchStr); 33 | } 34 | 35 | render() { 36 | const { lock, placeholder, tabIndex } = this.props; 37 | 38 | return ( 39 |
    40 | 43 | 51 |
    52 | ); 53 | } 54 | } 55 | 56 | PhoneNumberPane.propTypes = { 57 | focusSubmit: React.PropTypes.func.isRequired, 58 | lock: React.PropTypes.object.isRequired, 59 | placeholder: React.PropTypes.string.isRequired, 60 | tabIndex: React.PropTypes.number.isRequired 61 | }; 62 | 63 | PhoneNumberPane.defaultProps = { 64 | tabIndex: 1 65 | }; 66 | -------------------------------------------------------------------------------- /src/cred/social/ask_social_network.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screen from '../../lock/screen'; 3 | import SocialButtonsPane from './social_buttons_pane'; 4 | import { renderSocialHeaderText } from '../../social/index'; 5 | import { renderSignedInConfirmation } from '../../lock/signed_in_confirmation'; 6 | 7 | export default class AskSocialNetwork extends Screen { 8 | 9 | constructor() { 10 | super("network"); 11 | } 12 | 13 | renderAuxiliaryPane(lock) { 14 | return renderSignedInConfirmation(lock); 15 | } 16 | 17 | render({lock}) { 18 | return( 19 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/cred/social/social_button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as l from '../../lock/index'; 3 | import { signIn } from '../../social/actions'; 4 | import { displayName, useBigButtons } from '../../social/index'; 5 | 6 | export default class SocialButton extends React.Component { 7 | render() { 8 | const { connection, disabled, lock, tabIndex } = this.props; 9 | 10 | let className = "auth0-lock-social-button"; 11 | if (useBigButtons(lock)) className += " auth0-lock-social-big-button"; 12 | 13 | return ( 14 | 27 | ); 28 | } 29 | 30 | handleClick() { 31 | const { lock, connection } = this.props; 32 | signIn(l.id(lock), connection); 33 | } 34 | } 35 | 36 | SocialButton.propTypes = { 37 | lock: React.PropTypes.object.isRequired, 38 | connection: React.PropTypes.object.isRequired, 39 | disabled: React.PropTypes.bool.isRequired, 40 | tabIndex: React.PropTypes.number 41 | }; 42 | 43 | SocialButton.defaultProps = { 44 | disabled: false, 45 | tabIndex: 1 46 | }; 47 | -------------------------------------------------------------------------------- /src/cred/social/social_buttons_pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SocialButton from './social_button'; 3 | import * as l from '../../lock/index'; 4 | import { useBigButtons } from '../../social/index'; 5 | export default class SocialButtonsPane extends React.Component { 6 | 7 | render() { 8 | const { lock, showLoading, smallButtonsHeader } = this.props; 9 | 10 | const header = !useBigButtons(lock) 11 | && smallButtonsHeader 12 | &&

    13 | {smallButtonsHeader} 14 |

    ; 15 | 16 | const buttons = l.ui.connections(lock).map(x => ( 17 | 18 | )); 19 | 20 | const loading = showLoading 21 | &&
    22 |
    23 |
    ; 24 | 25 | return ( 26 |
    27 | {header} 28 |
    {buttons}
    29 | {loading} 30 |
    31 | ); 32 | } 33 | 34 | } 35 | 36 | SocialButtonsPane.propTypes = { 37 | lock: React.PropTypes.object.isRequired, 38 | showLoading: React.PropTypes.bool.isRequired, 39 | smallButtonsHeader: React.PropTypes.string 40 | }; 41 | 42 | SocialButtonsPane.defaultProps = { 43 | showLoading: false 44 | }; 45 | -------------------------------------------------------------------------------- /src/cred/storage.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map } from 'immutable'; 2 | 3 | function prefixKey(key) { 4 | return `${key}Cred`; 5 | } 6 | 7 | export function store(m, cred, key) { 8 | try { 9 | const value = Map().set(cred, m.getIn(["cred", cred])).toJS(); 10 | global.localStorage.setItem(prefixKey(key), global.JSON.stringify(value)); 11 | } catch (e) { 12 | // silently fail for now... 13 | } 14 | } 15 | 16 | export function restore(m, key) { 17 | try { 18 | const item = global.localStorage.getItem(prefixKey(key)); 19 | if (item) { 20 | const cred = Immutable.fromJS(global.JSON.parse(item)); 21 | return m.set("cred", cred); 22 | } 23 | } catch (e) { 24 | // silently fail for now... 25 | } 26 | 27 | return m; 28 | } 29 | -------------------------------------------------------------------------------- /src/cred/vcode/actions.js: -------------------------------------------------------------------------------- 1 | import { swap, updateEntity } from '../../store/index'; 2 | import * as c from '../index'; 3 | 4 | export function changeVcode(id, vcode) { 5 | swap(updateEntity, "lock", id, c.setVcode, vcode); 6 | } 7 | -------------------------------------------------------------------------------- /src/cred/vcode/ask_vcode.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screen from '../../lock/screen'; 3 | import VcodePane from './vcode_pane'; 4 | 5 | export default class AskVcode extends Screen { 6 | 7 | constructor() { 8 | super("code"); 9 | } 10 | 11 | render({lock}) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/cred/vcode/vcode_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InputWrap from '../input_wrap'; 3 | import Icon from '../../icon/icon'; 4 | import { isSmallScreen } from '../../utils/media_utils'; 5 | 6 | export default class VcodeInput extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | componentDidMount() { 13 | if (!isSmallScreen()) { 14 | // TODO: We can't set the focus immediately because we have to wait for 15 | // the input to be visible. Use a more robust solution (Placeholder should 16 | // notify it children when they are being shown). 17 | setTimeout(() => this.refs.input.focus(), 1200); 18 | } 19 | } 20 | 21 | render() { 22 | const { isValid, ...props } = this.props; 23 | const { focused } = this.state; 24 | 25 | return ( 26 | } focused={focused}> 27 | 36 | 37 | ); 38 | } 39 | 40 | handleFocus() { 41 | this.setState({focused: true}); 42 | } 43 | 44 | handleBlur() { 45 | this.setState({focused: false}); 46 | } 47 | } 48 | 49 | // TODO: specify propTypes 50 | -------------------------------------------------------------------------------- /src/cred/vcode/vcode_pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import VcodeInput from './vcode_input'; 3 | import * as l from '../../lock/index'; 4 | import * as c from '../index'; 5 | import { isSmallScreen } from '../../utils/media_utils'; 6 | import { changeVcode } from './actions'; 7 | 8 | // TODO: remove passwordless deps 9 | import { back } from '../../passwordless/actions'; 10 | 11 | export default class VcodePane extends React.Component { 12 | 13 | handleVcodeChange(e) { 14 | e.preventDefault(); 15 | changeVcode(l.id(this.props.lock), e.target.value); 16 | } 17 | 18 | handleResendClick(e) { 19 | e.preventDefault(); 20 | back(l.id(this.props.lock), {clearCred: ["vcode"]}); 21 | } 22 | 23 | render() { 24 | const { lock, placeholder, resendLabel, tabIndex } = this.props; 25 | 26 | return ( 27 |
    28 | 35 |

    36 | 37 | {resendLabel} 38 | 39 |

    40 |
    41 | ); 42 | } 43 | 44 | } 45 | 46 | VcodePane.propTypes = { 47 | lock: React.PropTypes.object.isRequired, 48 | placeholder: React.PropTypes.string.isRequired, 49 | resendLabel: React.PropTypes.string.isRequired, 50 | tabIndex: React.PropTypes.number.isRequired 51 | }; 52 | 53 | VcodePane.defaultProps = { 54 | tabIndex: 1 55 | }; 56 | -------------------------------------------------------------------------------- /src/dict/dicts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | emailcode: { 3 | code: { 4 | codeInputPlaceholder: "Your code", 5 | footerText: "", 6 | headerText: "An email with the code has been sent to {email}.", 7 | resendLabel: "Did not get the code?" 8 | }, 9 | signedIn: { 10 | success: "Thanks for signing in." 11 | }, 12 | email: { 13 | emailInputPlaceholder: "yours@example.com", 14 | footerText: "", 15 | headerText: "Enter your email to sign in
    or create an account" 16 | }, 17 | title: "Auth0", 18 | welcome: "Welcome {name}!" 19 | }, 20 | error: { 21 | passwordless: { 22 | "bad.tenant": "We're sorry, we can't send you the {medium} because there's a configuration problem", 23 | "bad.client_id": "We're sorry, we can't send you the {medium} because there's a configuration problem", 24 | "bad.connection": "We're sorry, we can't send you the {medium} because there's a configuration problem", 25 | "bad.send": "We're sorry, we can't send you the {medium} because there's an internal error", 26 | "bad.authParams": "We're sorry, we can't send you the {medium} because there's an internal error", 27 | "bad.request": "We're sorry, we can't send you the {medium} because there's an internal error", 28 | "bad.email": "The email is invalid", 29 | "bad.phone_number": "The phone number is invalid", 30 | "lock.request": "We're sorry, something went wrong when sending the {medium}", 31 | "sms_provider_error": "We're sorry, something went wrong when sending the SMS", 32 | "sms_provider_error.bad_phone_number": "The number {phoneNumber} is not a valid phone number" 33 | }, 34 | signIn: { 35 | "invalid_user_password": "Wrong {cred} or verification code", 36 | "lock.popup_closed": "Popup window closed. Try again.", 37 | "lock.request": "We're sorry, something went wrong when attempting to sign in", 38 | "lock.unauthorized": "Permissions were not granted. Try again." 39 | } 40 | }, 41 | magiclink: { 42 | emailSent: { 43 | failedLabel: "Failed!", 44 | retryLabel: "Retry", 45 | resendLabel: "Resend", 46 | resendingLabel: "Resending...", 47 | sentLabel: "Sent!", 48 | success: "We sent you a link to sign in
    to {email}." 49 | }, 50 | email: { 51 | emailInputPlaceholder: "yours@example.com", 52 | footerText: "", 53 | headerText: "Enter your email to sign in
    or create an account" 54 | }, 55 | title: "Auth0", 56 | welcome: "Welcome {name}!" 57 | }, 58 | sms: { 59 | code: { 60 | codeInputPlaceholder: "Your code", 61 | footerText: "", 62 | headerText: "An SMS with the code has been sent to {phoneNumber}.", 63 | resendLabel: "Did not get the code?" 64 | }, 65 | signedIn: { 66 | success: "Thanks for signing in." 67 | }, 68 | location: { 69 | locationFilterInputPlaceholder: "Select your country" 70 | }, 71 | phone: { 72 | footerText: "", 73 | headerText: "Enter your phone to sign in
    or create an account", 74 | phoneNumberInputPlaceholder: "your phone number" 75 | }, 76 | title: "Auth0" 77 | }, 78 | social: { 79 | network: { 80 | footerText: "", 81 | headerText: "", 82 | smallSocialButtonsHeader: "Login with" 83 | }, 84 | signedIn: { 85 | success: "Thanks for signing in." 86 | }, 87 | title: "Auth0" 88 | }, 89 | socialOrEmailcode: { 90 | code: { 91 | codeInputPlaceholder: "Your code", 92 | footerText: "", 93 | headerText: "An email with the code has been sent to {email}.", 94 | resendLabel: "Did not get the code?" 95 | }, 96 | networkOrEmail: { 97 | emailInputPlaceholder: "yours@example.com", 98 | footerText: "", 99 | headerText: "", 100 | separatorText: "Otherwise, enter your email to sign in
    or create an account", 101 | smallSocialButtonsHeader: "Login with" 102 | }, 103 | signedIn: { 104 | success: "Thanks for signing in." 105 | }, 106 | title: "Auth0", 107 | welcome: "Welcome {name}!" 108 | }, 109 | socialOrMagiclink: { 110 | emailSent: { 111 | failedLabel: "Failed!", 112 | retryLabel: "Retry", 113 | resendLabel: "Resend", 114 | resendingLabel: "Resending...", 115 | sentLabel: "Sent!", 116 | success: "We sent you a link to sign in
    to {email}." 117 | }, 118 | networkOrEmail: { 119 | emailInputPlaceholder: "yours@example.com", 120 | footerText: "", 121 | headerText: "", 122 | separatorText: "Otherwise, enter your email to sign in
    or create an account", 123 | smallSocialButtonsHeader: "Login with" 124 | }, 125 | signedIn: { 126 | success: "Thanks for signing in." 127 | }, 128 | title: "Auth0", 129 | welcome: "Welcome {name}!" 130 | }, 131 | socialOrSms: { 132 | code: { 133 | codeInputPlaceholder: "Your code", 134 | footerText: "", 135 | headerText: "An SMS with the code has been sent to {phoneNumber}.", 136 | resendLabel: "Did not get the code?" 137 | }, 138 | signedIn: { 139 | success: "Thanks for signing in." 140 | }, 141 | location: { 142 | locationFilterInputPlaceholder: "Select your country" 143 | }, 144 | networkOrPhone: { 145 | footerText: "", 146 | headerText: "", 147 | phoneNumberInputPlaceholder: "your phone number", 148 | separatorText: "Otherwise, enter your phone to sign in
    or create an account", 149 | smallSocialButtonsHeader: "Login with" 150 | }, 151 | title: "Auth0", 152 | welcome: "Welcome {name}!" 153 | } 154 | }; 155 | -------------------------------------------------------------------------------- /src/dict/index.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map } from 'immutable'; 2 | import dicts from './dicts'; 3 | 4 | class Dict { 5 | constructor(dict) { 6 | this.dict = dict; 7 | } 8 | 9 | get(keyPath, params = {}) { 10 | return Immutable.fromJS(params).reduce((r, v, k) => { 11 | return r.replace(`{${k}}`, v); 12 | }, this.dict.getIn(keyPath, "")); 13 | } 14 | } 15 | 16 | export function build(dictName, overrides) { 17 | overrides = Immutable.fromJS(overrides); 18 | const dict = Immutable.fromJS(dicts).get(dictName, Map()).set("error", Immutable.fromJS(dicts.error)); 19 | return new Dict(dict.mergeDeep(overrides)); 20 | } 21 | -------------------------------------------------------------------------------- /src/dict/t.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function(dict, keyPath, params = {}) { 4 | const html = dict.get(keyPath, params); 5 | if (!html) { 6 | return null; 7 | } 8 | 9 | if (params.__textOnly) { 10 | return html; 11 | } 12 | 13 | return React.createElement("span", {dangerouslySetInnerHTML: {__html: html}}); 14 | } 15 | -------------------------------------------------------------------------------- /src/gravatar/actions.js: -------------------------------------------------------------------------------- 1 | import { read, swap, getEntity, updateEntity } from '../store/index'; 2 | import * as g from './index'; 3 | import * as webAPI from './web_api'; 4 | import * as f from '../utils/fn_utils'; 5 | 6 | export function requestGravatar(email) { 7 | email = g.normalizeGravatarEmail(email); 8 | 9 | if (!read(getEntity, "gravatar", email)) { 10 | requestGravatarImage(email); 11 | requestGravatarDisplayName(email); 12 | } 13 | } 14 | 15 | export const debouncedRequestGravatar = f.debounce(requestGravatar, 300); 16 | 17 | function requestGravatarDisplayName(email) { 18 | const success = (email, entry) => requestGravatarDisplayNameSuccess(email, entry.displayName); 19 | const error = email => requestGravatarDisplayNameError(email); 20 | webAPI.profile(email, success, error); 21 | } 22 | 23 | function requestGravatarImage(email) { 24 | const success = (email, img) => requestGravatarImageSuccess(email, img.src); 25 | const error = email => requestGravatarImageError(email); 26 | webAPI.img(email, success, error); 27 | } 28 | 29 | 30 | function requestGravatarDisplayNameSuccess(email, displayName) { 31 | swap(updateEntity, "gravatar", email, g.setDisplayName, displayName); 32 | } 33 | 34 | function requestGravatarDisplayNameError(email) {} 35 | 36 | function requestGravatarImageSuccess(email, imageUrl) { 37 | swap(updateEntity, "gravatar", email, g.setImageUrl, imageUrl); 38 | } 39 | 40 | function requestGravatarImageError(email) {} 41 | -------------------------------------------------------------------------------- /src/gravatar/index.js: -------------------------------------------------------------------------------- 1 | import trim from 'trim'; 2 | 3 | export function setDisplayName(m, value) { 4 | return m.set("displayName", value); 5 | } 6 | 7 | export function displayName(m) { 8 | return m.get("displayName"); 9 | } 10 | 11 | export function setImageUrl(m, value) { 12 | return m.set("imageUrl", value); 13 | } 14 | 15 | export function imageUrl(m) { 16 | return m.get("imageUrl"); 17 | } 18 | 19 | export function loaded(m) { 20 | return !!(displayName(m) && imageUrl(m)); 21 | } 22 | 23 | export function normalizeGravatarEmail(str) { 24 | return typeof str === "string" 25 | ? trim(str.toLowerCase()) 26 | : ""; 27 | } 28 | -------------------------------------------------------------------------------- /src/gravatar/web_api.js: -------------------------------------------------------------------------------- 1 | import blueimp from 'blueimp-md5'; 2 | import { validateEmail } from '../cred/index'; 3 | import jsonp from '../utils/jsonp_utils'; 4 | import * as preload from '../preload/index'; 5 | 6 | const md5 = blueimp.md5 || blueimp; 7 | 8 | export function profile(email, success, error) { 9 | if (validateEmail(email)) { 10 | const url = `https://secure.gravatar.com/${md5(email)}.json`; 11 | jsonp.get(url, function (err, obj) { 12 | if (err) { 13 | error(email, err); 14 | } else if (obj && obj.entry && obj.entry[0]) { 15 | success(email, obj.entry[0]); 16 | } else { 17 | error(email); 18 | } 19 | }); 20 | } else { 21 | error(email); 22 | } 23 | } 24 | 25 | export function img(email, success, error) { 26 | if (validateEmail(email)) { 27 | const url = `https://secure.gravatar.com/avatar/${md5(email)}?d=404`; 28 | preload.img(url, function(err, img) { 29 | err ? error(email) : success(email, img); 30 | }); 31 | } else { 32 | error(email); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/header/background.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isSmallScreen } from '../utils/media_utils'; 3 | 4 | const cssBlurSupport = (function() { 5 | // Check stolen from Modernizr, see https://github.com/Modernizr/Modernizr/blob/29eab707f7a2fb261c8a9c538370e97eb1f86e25/feature-detects/css/filters.js 6 | const el = global.document.createElement('div'); 7 | el.style.cssText = "filter: blur(2px); -webkit-filter: blur(2px)"; 8 | return !!el.style.length && (global.document.documentMode === undefined || global.document.documentMode > 9); 9 | })(); 10 | 11 | export default class Background extends React.Component { 12 | render() { 13 | const { backgroundColor, imageUrl, grayScale } = this.props; 14 | 15 | const props = { 16 | className: "auth0-lock-header-bg" 17 | }; 18 | 19 | if (cssBlurSupport) { 20 | props.className += " auth0-lock-blur-support"; 21 | } 22 | 23 | const blurProps = { 24 | className: 'auth0-lock-header-bg-blur', 25 | style: {backgroundImage: `url('${imageUrl}')`} 26 | }; 27 | 28 | if (grayScale) { 29 | blurProps.className += ' auth0-lock-no-grayscale'; 30 | } 31 | 32 | const solidProps = { 33 | className: "auth0-lock-header-bg-solid", 34 | style: {backgroundColor: backgroundColor} 35 | } 36 | 37 | return ( 38 |
    39 |
    40 |
    41 |
    42 | ); 43 | } 44 | } 45 | 46 | Background.propTypes = { 47 | backgorundColor: React.PropTypes.string, 48 | grayScale: React.PropTypes.bool, 49 | imageUrl: React.PropTypes.string 50 | } 51 | -------------------------------------------------------------------------------- /src/header/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Background from './background'; 3 | import Welcome from './welcome'; 4 | import IconButton from '../icon/button'; 5 | 6 | export default class Header extends React.Component { 7 | render() { 8 | const { backHandler, backgroundColor, backgroundUrl, logoUrl, name, title } = this.props; 9 | 10 | return ( 11 |
    12 | {backHandler && } 13 | 14 | 15 |
    16 | ); 17 | } 18 | } 19 | 20 | Header.propTypes = { 21 | backgroundUrl: React.PropTypes.string, 22 | logoUrl: React.PropTypes.string, 23 | name: React.PropTypes.string 24 | }; 25 | -------------------------------------------------------------------------------- /src/header/icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Icon extends React.Component { 4 | render() { 5 | const { imageUrl } = this.props; 6 | return ; 7 | } 8 | } 9 | 10 | Icon.propTyes = { 11 | imageUrl: React.PropTypes.string 12 | }; 13 | -------------------------------------------------------------------------------- /src/header/welcome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './icon'; 3 | import WelcomeMessage from './welcome_message'; 4 | 5 | export default class Welcome extends React.Component { 6 | render() { 7 | const { name, imageUrl, title } = this.props; 8 | 9 | return ( 10 |
    11 | {imageUrl && } 12 | 13 |
    14 | ); 15 | } 16 | } 17 | 18 | Welcome.propTypes = { 19 | imageUrl: React.PropTypes.string, 20 | name: React.PropTypes.string 21 | }; 22 | -------------------------------------------------------------------------------- /src/header/welcome_message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class WelcomeMessage extends React.Component { 4 | render() { 5 | const { name, title } = this.props; 6 | let className, message; 7 | 8 | if (name) { 9 | className = "auth0-lock-firstname"; 10 | message = name; 11 | } else { 12 | className = "auth0-lock-name"; 13 | message = title; 14 | } 15 | 16 | return
    {message}
    ; 17 | } 18 | } 19 | 20 | WelcomeMessage.propTypes = { 21 | name: React.PropTypes.string 22 | } 23 | -------------------------------------------------------------------------------- /src/icon/button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './icon'; 3 | 4 | export default class IconButton extends React.Component { 5 | render() { 6 | const { name } = this.props; 7 | const className = `auth0-lock-${name}-button`; 8 | 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | handleClick(e) { 17 | e.preventDefault(); 18 | this.props.onClick(); 19 | } 20 | } 21 | 22 | IconButton.propTypes = { 23 | name: React.PropTypes.string.isRequired, 24 | onClick: React.PropTypes.func.isRequired 25 | }; 26 | -------------------------------------------------------------------------------- /src/icon/icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { iconTag } from './index'; 3 | 4 | export default class Icon extends React.Component { 5 | render() { 6 | return ; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import RenderScheduler from './lock/render_scheduler'; 2 | import Renderer from './lock/renderer'; 3 | import PluginManager from './lock/plugin_manager'; 4 | import * as idu from './utils/id_utils'; 5 | import { closeLock, removeLock, setupLock, updateLock } from './lock/actions'; 6 | import { requestGravatar } from './gravatar/actions'; 7 | import webAPI from './lock/web_api'; 8 | import emailcodeSpec from './mode/emailcode/spec'; 9 | import magiclinkSpec from './mode/magiclink/spec'; 10 | import smsSpec from './mode/sms/spec'; 11 | import socialSpec from './mode/social/spec'; 12 | import socialOrEmailcodeSpec from './mode/social-or-emailcode/spec'; 13 | import socialOrMagiclinkSpec from './mode/social-or-magiclink/spec'; 14 | import socialOrSmsSpec from './mode/social-or-sms/spec'; 15 | 16 | // telemetry 17 | import Auth0 from 'auth0-js'; 18 | 19 | import css from '../css/index.css'; 20 | 21 | const head = document.getElementsByTagName('head')[0]; 22 | const style = document.createElement('style'); 23 | style.type = 'text/css'; 24 | head.appendChild(style); 25 | if (style.styleSheet) { 26 | style.styleSheet.cssText = css; 27 | } else { 28 | style.appendChild(document.createTextNode(css)); 29 | } 30 | 31 | export default class Auth0LockPasswordless { 32 | constructor(clientID, domain) { 33 | if (typeof clientID != "string") { 34 | throw new Error("A `clientID` string must be provided as first argument."); 35 | } 36 | if (typeof domain != "string") { 37 | throw new Error("A `domain` string must be provided as second argument."); 38 | } 39 | 40 | this.id = idu.incremental(); 41 | setupLock(this.id, clientID, domain); 42 | } 43 | 44 | close() { 45 | const f = Auth0LockPasswordless.plugins.closeFn(this.plugin); 46 | f(this.id, true); 47 | } 48 | 49 | destroy() { 50 | removeLock(this.id); 51 | } 52 | 53 | getProfile(token, cb) { 54 | return webAPI.getProfile(this.id, token, cb); 55 | } 56 | 57 | parseHash(hash) { 58 | return webAPI.parseHash(this.id, hash); 59 | } 60 | 61 | logout(query = {}) { 62 | webAPI.signOut(this.id, query); 63 | } 64 | 65 | update(f) { 66 | return updateLock(this.id, f); 67 | } 68 | 69 | requestGravatar(email) { 70 | return requestGravatar(email); 71 | } 72 | } 73 | 74 | Auth0LockPasswordless.plugins = new PluginManager(Auth0LockPasswordless.prototype); 75 | Auth0LockPasswordless.renderer = new Renderer(); 76 | Auth0LockPasswordless.renderScheduler = new RenderScheduler(Auth0LockPasswordless); 77 | 78 | Auth0LockPasswordless.plugins.register(emailcodeSpec); 79 | Auth0LockPasswordless.plugins.register(magiclinkSpec); 80 | Auth0LockPasswordless.plugins.register(smsSpec); 81 | Auth0LockPasswordless.plugins.register(socialSpec); 82 | Auth0LockPasswordless.plugins.register(socialOrEmailcodeSpec); 83 | Auth0LockPasswordless.plugins.register(socialOrMagiclinkSpec); 84 | Auth0LockPasswordless.plugins.register(socialOrSmsSpec); 85 | 86 | 87 | // telemetry 88 | Auth0LockPasswordless.version = __VERSION__; 89 | Auth0.clientInfo.lib_version = Auth0.clientInfo.version; 90 | Auth0.clientInfo.name = "lock-passwordless.js"; 91 | Auth0.clientInfo.version = Auth0LockPasswordless.version; 92 | -------------------------------------------------------------------------------- /src/lock/actions.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import WebAPI from './web_api'; 3 | import { getEntity, read, removeEntity, swap, setEntity, updateEntity } from '../store/index'; 4 | import * as l from './index'; 5 | import * as cs from '../cred/storage'; 6 | 7 | export function setupLock(id, clientID, domain) { 8 | const lock = l.setup({id: id, clientID: clientID, domain: domain}); 9 | swap(setEntity, "lock", id, lock); 10 | 11 | WebAPI.setupClient(id, clientID, domain); 12 | 13 | // TODO: only modes with a phone number are making use of the user's location 14 | const user = read(getEntity, "user"); 15 | const location = user && user.get("location"); 16 | 17 | if (!location) { 18 | WebAPI.getUserCountry(id, (err, isoCode) => { 19 | if (!err) { 20 | swap(updateEntity, "user", 0, m => m.set("location", isoCode)); 21 | } 22 | }); 23 | } 24 | } 25 | 26 | export function openLock(id, modeName, options) { 27 | const lock = read(getEntity, "lock", id); 28 | if (!lock) { 29 | throw new Error("The Lock can't be opened again after it has been destroyed"); 30 | } 31 | 32 | if (l.show(lock)) { 33 | return false; 34 | } 35 | 36 | swap(updateEntity, "lock", id, lock => { 37 | lock = l.render(lock, modeName, options); 38 | 39 | return l.ui.rememberLastLogin(lock) 40 | ? cs.restore(lock, l.modeName(lock)) 41 | : lock; 42 | }); 43 | 44 | setTimeout(() => swap(updateEntity, "lock", id, l.setShow, true), 17); 45 | return true; 46 | } 47 | 48 | export function closeLock(id, force = false, callback = () => {}) { 49 | // Do nothing when the Lock can't be closed, unless closing is forced. 50 | let lock = read(getEntity, "lock", id); 51 | if (!l.ui.closable(lock) && !force) { 52 | return; 53 | } 54 | 55 | // Close the Lock. Also, stop rendering when in inline mode. In modal mode we 56 | // need to wait for the close animation to finish before stop rendering the 57 | // Lock. 58 | swap(updateEntity, "lock", id, lock => { 59 | if (!l.ui.appendContainer(lock)) { 60 | lock = lock.remove("render"); 61 | } 62 | 63 | return l.close(lock) 64 | }); 65 | 66 | // If we are still rendering (modal mode), schedule a function that will 67 | // execute the callback and destroy the Lock (liberate its resources). If we 68 | // are not rendering (inline mode), do both things immediately. 69 | lock = read(getEntity, "lock", id); 70 | 71 | if (l.rendering(lock)) { 72 | setTimeout(() => { 73 | // swap(updateEntity, "lock", id, m => m.remove("render")); 74 | callback(read(getEntity, "lock", id)); 75 | setTimeout(() => swap(updateEntity, "lock", id, l.reset), 17); 76 | }, 1000); 77 | } else { 78 | swap(updateEntity, "lock", id, l.reset); 79 | callback(lock); 80 | } 81 | } 82 | 83 | export function removeLock(id) { 84 | swap(updateEntity, "lock", id, (lock) => lock.remove("render")); 85 | swap(removeEntity, "lock", id); 86 | } 87 | 88 | export function updateLock(id, f) { 89 | return swap(updateEntity, "lock", id, f); 90 | } 91 | 92 | export function registerMode(spec) { 93 | swap(setEntity, "mode", spec.name, Immutable.fromJS(spec)); 94 | } 95 | -------------------------------------------------------------------------------- /src/lock/avatar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Avatar extends React.Component { 4 | render() { 5 | const { imageUrl } = this.props; 6 | return ; 7 | } 8 | } 9 | 10 | Avatar.propTypes = { 11 | imageUrl: React.PropTypes.string 12 | } 13 | -------------------------------------------------------------------------------- /src/lock/badge.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '../icon/icon'; 3 | 4 | export default class Badge extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lock/chrome.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactTransitionGroup from 'react-addons-transition-group'; 4 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 5 | import GlobalError from './global_error'; 6 | import SubmitButton from './submit_button'; 7 | import Header from '../header/header'; 8 | import * as l from './index'; 9 | import * as g from '../gravatar/index'; 10 | import Terms from '../lock/terms'; 11 | 12 | 13 | export default class Chrome extends React.Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = {height: "", show: true}; 18 | } 19 | 20 | componentDidMount() { 21 | this.reverse = false; 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (!l.globalError(this.props.lock) && l.globalError(nextProps.lock)) { 26 | this.setState({height: "auto"}); 27 | } 28 | } 29 | 30 | render() { 31 | const { auxiliaryPane, backHandler, contentRender, headerText, footerText, lock, showSubmitButton } = this.props; 32 | const { height, show } = this.state; 33 | 34 | const gravatar = l.gravatar(lock); 35 | const icon = l.ui.icon(lock); 36 | const globalError = l.globalError(lock) || null; 37 | const disableSubmit = l.submitting(lock); 38 | 39 | let backgroundUrl, name; 40 | if (gravatar) { 41 | backgroundUrl = g.imageUrl(gravatar); 42 | name = this.t(["welcome"], {name: g.displayName(gravatar), __textOnly: true}); 43 | } else { 44 | backgroundUrl = icon; 45 | name = ""; 46 | } 47 | const primaryColor = l.ui.primaryColor(lock); 48 | 49 | const header = headerText &&

    {headerText}

    ; 50 | const footer = footerText && {footerText}; 51 | 52 | return ( 53 |
    54 |
    55 | 56 | 57 | {globalError && } 58 | 59 |
    60 |
    61 | {header} 62 | {contentRender({focusSubmit: ::this.focusSubmit, lock})} 63 |
    64 |
    65 | {footer} 66 |
    67 | {showSubmitButton && } 68 | 69 | {auxiliaryPane} 70 | 71 |
    72 | ); 73 | } 74 | 75 | focusSubmit() { 76 | this.refs.submit.focus(); 77 | } 78 | 79 | handleBack() { 80 | const { backHandler, lock } = this.props; 81 | this.reverse = true; 82 | backHandler(l.id(lock)); 83 | } 84 | 85 | componentWillSlideIn(slide) { 86 | const node = ReactDOM.findDOMNode(this.refs.content); 87 | this.originalHeight = parseInt(window.getComputedStyle(node, null).height, 10); 88 | this.setState({height: slide.height, show: false}); 89 | } 90 | 91 | componentDidSlideIn() { 92 | this.setState({height: this.originalHeight}); 93 | setTimeout(() => this.setState({show: true}), 500); 94 | } 95 | 96 | componentWillSlideOut(callback) { 97 | const node = ReactDOM.findDOMNode(this.refs.content); 98 | const size = window.getComputedStyle(node, null).height; 99 | callback({height: parseInt(size, 10), reverse: this.reverse}); 100 | } 101 | 102 | t(keyPath, params) { 103 | return l.ui.t(this.props.lock, keyPath, params); 104 | } 105 | } 106 | 107 | Chrome.propTypes = { 108 | auxiliaryPane: React.PropTypes.element, 109 | backHandler: React.PropTypes.func, 110 | contentRender: React.PropTypes.func.isRequired, 111 | footerText: React.PropTypes.element, 112 | headerText: React.PropTypes.element, 113 | lock: React.PropTypes.object.isRequired, 114 | showSubmitButton: React.PropTypes.bool.isRequired 115 | }; 116 | 117 | Chrome.defaultProps = { 118 | showSubmitButton: true 119 | }; 120 | 121 | class Placeholder extends React.Component { 122 | constructor(props) { 123 | super(props); 124 | this.state = {}; 125 | } 126 | 127 | componentWillReceiveProps(nextProps) { 128 | if (nextProps.height) { 129 | 130 | if (!this.state.height || nextProps.height === "auto") { 131 | this.setState({height: nextProps.height}); 132 | return; 133 | } 134 | 135 | // TODO: Instead of storing a flag to indicate whether or not we are 136 | // performing an animation, we should store the height we are going to. 137 | // Also use rAF. 138 | if (this.state.height != nextProps.height && !this.state.animating) { 139 | const frames = 10; 140 | let count = 0; 141 | let current = parseInt(this.state.height, 10); 142 | const last = parseInt(nextProps.height, 10); 143 | const step = Math.abs(current - last) / frames; 144 | const dir = current < last ? 1 : -1; 145 | const dh = step * dir; 146 | 147 | this.t = setInterval(() => { 148 | if (count < frames - 1) { 149 | this.setState({height: current, animating: true}); 150 | current += dh; 151 | count++; 152 | } else { 153 | clearInterval(this.t); 154 | delete this.t; 155 | this.setState({height: last, animating: false}); 156 | } 157 | }, 17); 158 | } 159 | } 160 | } 161 | 162 | componentWillUnmount() { 163 | if (this.t) { 164 | clearInterval(this.t); 165 | } 166 | } 167 | 168 | render() { 169 | const { children, delay, height, show } = this.props; 170 | const style = this.state.height ? {height: this.state.height} : {}; 171 | 172 | return ( 173 |
    174 |
    175 | {children} 176 |
    177 |
    178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/lock/confirmation_pane.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '../icon/icon'; 3 | import IconButton from '../icon/button'; 4 | 5 | export default class ConfirmationPane extends React.Component { 6 | 7 | render() { 8 | const { backHandler, closeHandler } = this.props; 9 | 10 | return ( 11 |
    12 | {closeHandler && } 13 | {backHandler && } 14 |
    15 | 16 | {this.props.children} 17 |
    18 |
    19 | ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/lock/container_manager.js: -------------------------------------------------------------------------------- 1 | export default class LockContainerManager { 2 | constructor() { 3 | this.cache = {}; 4 | } 5 | 6 | ensure(id, create) { 7 | let container = this.cache[id]; 8 | if (!container) { 9 | container = this.cache[id] = document.getElementById(id); 10 | } 11 | if (!container) { 12 | if (create) { 13 | container = document.createElement('div'); 14 | container.id = id; 15 | container.className = "auth0-lock-container"; 16 | this.cache[id] = document.body.appendChild(container); 17 | } else { 18 | throw new Error(`Can't find element with id ${id}`); 19 | } 20 | } 21 | return container; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lock/global_error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export default class GlobalError extends React.Component { 5 | render() { 6 | return ( 7 |
    8 | {this.props.message} 9 |
    10 | ); 11 | } 12 | 13 | // componentWillAppear(callback) { 14 | // console.log("componentWillAppear") 15 | // callback(); 16 | // } 17 | // 18 | // componentDidAppear() { 19 | // console.log("componentDidAppear"); 20 | // } 21 | 22 | componentWillEnter(callback) { 23 | // console.log("componentWillEnter"); 24 | const node = ReactDOM.findDOMNode(this); 25 | var computedStyle = window.getComputedStyle(node, null); 26 | var height = computedStyle.height; 27 | var paddingTop = computedStyle.paddingTop; 28 | var paddingBottom = computedStyle.paddingBottom; 29 | node.style.height = "0px"; 30 | node.style.paddingTop = "0px"; 31 | node.style.paddingBottom = "0px"; 32 | setTimeout(function() { 33 | node.style.transition = "all 0.2s"; 34 | node.style.height = height; 35 | node.style.paddingTop = paddingTop; 36 | node.style.paddingBottom = paddingBottom; 37 | callback(); 38 | }, 17); 39 | } 40 | 41 | // componentDidEnter() { 42 | // console.log("componentDidEnter"); 43 | // } 44 | 45 | componentWillLeave(callback) { 46 | // console.log("componentWillLeave"); 47 | const node = ReactDOM.findDOMNode(this); 48 | node.style.transition = "all 0.2s"; 49 | node.style.height = "0px"; 50 | node.style.paddingTop = "0px"; 51 | node.style.paddingBottom = "0px"; 52 | setTimeout(function() { 53 | node.style.removeProperty("transition"); 54 | node.style.removeProperty("height"); 55 | node.style.removeProperty("padding-top"); 56 | node.style.removeProperty("padding-bottom"); 57 | callback(); 58 | }, 250); 59 | } 60 | 61 | // componentDidLeave() { 62 | // console.log("componentDidLeave"); 63 | // } 64 | } 65 | 66 | GlobalError.propTypes = { 67 | message: React.PropTypes.string.isRequired 68 | } 69 | -------------------------------------------------------------------------------- /src/lock/index.js: -------------------------------------------------------------------------------- 1 | import Immutable, { List, Map, Set } from 'immutable'; 2 | import { isSmallScreen } from '../utils/media_utils'; 3 | import { iconUrl } from '../icon/index'; 4 | import * as d from '../dict/index'; 5 | import t from '../dict/t'; 6 | 7 | function buildSetupSnapshot(m) { 8 | return m.set("setupSnapshot", m); 9 | } 10 | 11 | export function setup(attrs) { 12 | const { clientID, domain, id } = attrs; 13 | return buildSetupSnapshot(Immutable.fromJS({ 14 | clientID: clientID, 15 | domain: domain, 16 | id: id 17 | })); 18 | } 19 | 20 | export function id(m) { 21 | return m.get("id"); 22 | } 23 | 24 | export function clientID(m) { 25 | return m.get("clientID"); 26 | } 27 | 28 | export function domain(m) { 29 | return m.get("domain"); 30 | } 31 | 32 | export function modeName(m) { 33 | return modeOptions(m).get("name"); 34 | } 35 | 36 | export function show(m) { 37 | return m.get("show", false); 38 | } 39 | 40 | export function setShow(m, value) { 41 | return m.set("show", value); 42 | } 43 | 44 | export function setSubmitting(m, value, error) { 45 | m = m.set("submitting", value); 46 | m = error && !value ? setGlobalError(m, error) : clearGlobalError(m); 47 | return m; 48 | } 49 | 50 | export function submitting(m) { 51 | return m.get("submitting", false); 52 | } 53 | 54 | export function setGlobalError(m, str) { 55 | return m.set("globalError", str); 56 | } 57 | 58 | export function globalError(m) { 59 | return m.get("globalError", ""); 60 | } 61 | 62 | export function clearGlobalError(m) { 63 | return m.remove("globalError"); 64 | } 65 | 66 | export function rendering(m) { 67 | return m.get("render", false); 68 | } 69 | 70 | export function gravatar(m) { 71 | if (ui.gravatar(m)) { 72 | return m.get("gravatar"); 73 | } else { 74 | return undefined; 75 | } 76 | } 77 | 78 | function extractUIOptions(id, modeName, options) { 79 | const closable = options.container ? false : undefined === options.closable ? true : !!options.closable; 80 | return new Map({ 81 | containerID: options.container || `auth0-lock-container-${id}`, 82 | appendContainer: !options.container, 83 | autoclose: undefined === options.autoclose ? false : closable && options.autoclose, 84 | icon: options.icon || "//cdn.auth0.com/styleguide/4.6.1/lib/logos/img/badge.png", 85 | closable: closable, 86 | connections: new List(undefined === options.connections ? [] : options.connections), 87 | dict: d.build(modeName, typeof options.dict === "object" ? options.dict : {}), 88 | focusInput: undefined === options.focusInput ? !(options.container || isSmallScreen()) : !!options.focusInput, 89 | gravatar: undefined === options.gravatar ? true : !!options.gravatar, 90 | mobile: undefined === options.mobile ? false : !!options.mobile, 91 | signInCallback: options.signInCallback, // TODO: this doesn't belong here 92 | popup: undefined === options.popup ? false : !!options.popup, 93 | popupOptions: new Map(undefined === options.popupOptions ? {} : options.popupOptions), 94 | primaryColor: options.primaryColor && typeof options.primaryColor === "string" ? options.primaryColor : "#ea5323", 95 | rememberLastLogin: undefined === options.rememberLastLogin ? true : !!options.rememberLastLogin 96 | }); 97 | } 98 | 99 | function setUIOptions(m, options) { 100 | let currentUIOptions = m.get("ui"); 101 | let newUIOptions = extractUIOptions(id(m), modeName(m), options);; 102 | if (currentUIOptions) { 103 | const denied = new Set(["containerID", "appendContainer"]); 104 | const provided = Set.fromKeys(options).subtract(denied); 105 | newUIOptions = newUIOptions.filter((v, k) => provided.has(k)); 106 | } 107 | return m.set("ui", (currentUIOptions || new Map()).merge(newUIOptions)); 108 | } 109 | 110 | function getUIAttribute(m, attribute) { 111 | return m.getIn(["ui", attribute]); 112 | } 113 | 114 | export const ui = { 115 | containerID: lock => getUIAttribute(lock, "containerID"), 116 | appendContainer: lock => getUIAttribute(lock, "appendContainer"), 117 | autoclose: lock => getUIAttribute(lock, "autoclose"), 118 | icon: lock => getUIAttribute(lock, "icon"), 119 | closable: lock => getUIAttribute(lock, "closable"), 120 | connections: lock => getUIAttribute(lock, "connections"), 121 | dict: lock => getUIAttribute(lock, "dict"), 122 | t: (lock, keyPath, params) => t(ui.dict(lock), keyPath, params), 123 | focusInput: lock => getUIAttribute(lock, "focusInput"), 124 | gravatar: lock => getUIAttribute(lock, "gravatar"), 125 | mobile: lock => getUIAttribute(lock, "mobile"), 126 | signInCallback: lock => getUIAttribute(lock, "signInCallback"), 127 | popup: lock => getUIAttribute(lock, "popup"), 128 | popupOptions: lock => getUIAttribute(lock, "popupOptions"), 129 | primaryColor: lock => getUIAttribute(lock, "primaryColor"), 130 | rememberLastLogin: lock => getUIAttribute(lock, "rememberLastLogin") 131 | }; 132 | 133 | function getLoginAttribute(m, attribute) { 134 | return m.getIn(["login", attribute]); 135 | } 136 | 137 | // TODO: find a better name, forceJSONP is not exclusively used for login 138 | export const login = { 139 | authParams: lock => getLoginAttribute(lock, "authParams"), 140 | forceJSONP: lock => getLoginAttribute(lock, "forceJSONP"), 141 | callbackURL: lock => getLoginAttribute(lock, "callbackURL"), 142 | responseType: lock => getLoginAttribute(lock, "responseType") 143 | // TODO: Add a function that takes an object with login parameters and adds 144 | // the ones above here. 145 | }; 146 | 147 | function setLoginOptions(m, options) { 148 | let { authParams, callbackURL, forceJSONP, responseType } = options; 149 | 150 | authParams = typeof authParams === "object" ? authParams : {}; 151 | callbackURL = typeof callbackURL === "string" && callbackURL ? callbackURL : undefined; 152 | responseType = typeof responseType === "string" ? responseType : callbackURL ? "code" : "token"; 153 | 154 | const loginOptions = Map({ 155 | authParams: Map(authParams), 156 | callbackURL: callbackURL, 157 | forceJSONP: forceJSONP, 158 | responseType: responseType 159 | }); 160 | 161 | return m.set("login", loginOptions); 162 | } 163 | 164 | export function invokeDoneCallback(m, ...args) { 165 | ui.signInCallback(m).apply(undefined, args); 166 | } 167 | 168 | export function shouldRedirect(m) { 169 | return m.get("forceRedirect", false) || login.callbackURL(m); 170 | } 171 | 172 | export function render(m, name, options) { 173 | if ((modeName(m) != undefined && modeName(m) != name) || show(m)) { 174 | return m; 175 | } 176 | 177 | const mode = options.mode || {}; 178 | mode.name = name; 179 | 180 | m = m.merge(Immutable.fromJS({ 181 | mode: mode, 182 | render: true 183 | })); 184 | 185 | m = setUIOptions(m, options); 186 | m = setLoginOptions(m, options); 187 | 188 | return m; 189 | } 190 | 191 | export function modeOptions(m) { 192 | return m.get("mode", new Map()); 193 | } 194 | 195 | export function close(m) { 196 | return m.set("show", false); 197 | } 198 | 199 | export function reset(m) { 200 | return buildSetupSnapshot(m.get("setupSnapshot")); 201 | } 202 | 203 | export function setSignedIn(m, value) { 204 | return m.set("signedIn", value); 205 | } 206 | 207 | export function signedIn(m) { 208 | return m.get("signedIn", false); 209 | } 210 | 211 | export function tabIndex(m, n) { 212 | return [id(m), n > 9 ? "" : "0", n].join(""); 213 | } 214 | -------------------------------------------------------------------------------- /src/lock/lock.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Map } from 'immutable'; 3 | import Chrome from './chrome'; 4 | import MultisizeSlide from '../multisize-slide/multisize_slide'; 5 | import Avatar from './avatar'; 6 | import IconButton from '../icon/button'; 7 | import Badge from './badge'; 8 | import * as l from './index'; 9 | import * as g from '../gravatar/index'; 10 | import EscKeydownUtils from '../utils/esc_keydown_utils'; 11 | 12 | export default class Lock extends React.Component { 13 | componentDidMount() { 14 | this.escKeydown = new EscKeydownUtils(() => this.handleEsc()); 15 | } 16 | 17 | componentWillUnmount() { 18 | this.escKeydown.release(); 19 | } 20 | 21 | render() { 22 | const { 23 | auxiliaryPane, 24 | backHandler, 25 | closeHandler, 26 | contentRender, 27 | disallowClose, 28 | footerText, 29 | headerText, 30 | lock, 31 | screenName, 32 | submitHandler 33 | } = this.props; 34 | 35 | const overlay = l.ui.appendContainer(lock) ? 36 |
    : null; 37 | 38 | const gravatar = l.gravatar(lock); 39 | const showCloseButton = l.ui.closable(lock) && !disallowClose; 40 | 41 | let className = "auth0-lock"; 42 | if (!l.ui.appendContainer(lock)) { 43 | className += " auth0-lock-opened-in-frame"; 44 | } else if (lock.get("show")) { 45 | className += " auth0-lock-opened"; 46 | } 47 | 48 | if (l.ui.mobile(lock)) { 49 | className += " auth0-lock-mobile"; 50 | } 51 | 52 | if (l.submitting(lock)) { 53 | className += " auth0-lock-mode-loading"; 54 | } 55 | 56 | if (auxiliaryPane) { 57 | className += " auth0-lock-auxiliary"; 58 | } 59 | 60 | return ( 61 |
    62 | {overlay} 63 |
    64 |
    65 | {gravatar && } 66 | {showCloseButton && } 67 |
    68 | 69 | 79 | 80 |
    81 | 82 | 83 | 84 | 85 |
    86 |
    87 | ); 88 | } 89 | 90 | handleSubmit(e) { 91 | e.preventDefault(); 92 | const { lock, submitHandler } = this.props; 93 | if (submitHandler) { 94 | submitHandler(l.id(lock)); 95 | } 96 | } 97 | 98 | handleClose() { 99 | const { closeHandler, lock } = this.props; 100 | if (!l.submitting(lock)) { 101 | closeHandler(l.id(lock)); 102 | } 103 | } 104 | 105 | handleEsc() { 106 | const { closeHandler, escHandler, lock } = this.props; 107 | escHandler ? escHandler(l.id(lock)) : this.handleClose(); 108 | } 109 | } 110 | 111 | Lock.propTypes = { 112 | auxiliaryPane: React.PropTypes.element, 113 | backHandler: React.PropTypes.func, 114 | contentRender: React.PropTypes.func.isRequired, 115 | footerText: React.PropTypes.element, 116 | headerText: React.PropTypes.element, 117 | lock: React.PropTypes.object.isRequired, 118 | screenName: React.PropTypes.string.isRequired, 119 | // closeHandler, 120 | // disallowClose, 121 | // escHandler 122 | // submitHandler, 123 | }; 124 | -------------------------------------------------------------------------------- /src/lock/pane_separator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PaneSeparator = ({children}) => ( 4 |

    {children}

    5 | ); 6 | 7 | PaneSeparator.propTypes = { 8 | children: React.PropTypes.oneOfType([ 9 | React.PropTypes.element, 10 | React.PropTypes.string 11 | ]).isRequired 12 | }; 13 | 14 | PaneSeparator.defaultProps = { 15 | children: "or" 16 | }; 17 | 18 | export default PaneSeparator; 19 | -------------------------------------------------------------------------------- /src/lock/plugin_manager.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map } from 'immutable'; 2 | 3 | export default class PluginManager { 4 | constructor(proto) { 5 | this.proto = proto; 6 | this.plugins = new Map({}); 7 | } 8 | 9 | register(pluginClass) { 10 | const plugin = new pluginClass(); 11 | const { name } = plugin; 12 | this.plugins = this.plugins.set(name, plugin); 13 | this.proto[name] = function(...args) { 14 | const isOpen = plugin.open(this.id, ...args); 15 | if (isOpen) { 16 | this.plugin = name; 17 | } 18 | 19 | return isOpen; 20 | } 21 | } 22 | 23 | renderFns() { 24 | return this.plugins.map(plugin => plugin.render); 25 | } 26 | 27 | closeFn(name) { 28 | return this.plugins.get(name).close; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lock/render_scheduler.js: -------------------------------------------------------------------------------- 1 | import { subscribe } from '../store/index'; 2 | 3 | export default class RenderScheduler { 4 | constructor(spec) { 5 | subscribe("main", (key, oldState, newState) => { 6 | spec.renderer.render(newState, spec.plugins.renderFns()); 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lock/renderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import CSSCore from 'fbjs/lib/CSSCore'; 4 | import ContainerManager from './container_manager'; 5 | import Lock from './lock'; 6 | import * as l from './index'; 7 | import * as c from '../cred/index'; 8 | import * as g from '../gravatar/index'; 9 | import { getCollection, getEntity } from '../store/index'; 10 | 11 | export default class Renderer { 12 | constructor() { 13 | this.containerManager = new ContainerManager(); 14 | } 15 | 16 | render(state, fns) { 17 | const locks = getCollection(state, "lock"); 18 | 19 | locks.forEach(lock => { 20 | if (l.rendering(lock)) { 21 | const gravatar = getEntity(state, "gravatar", g.normalizeGravatarEmail(c.email(lock))); 22 | lock = lock.set("gravatar", gravatar && g.loaded(gravatar) ? gravatar : null); 23 | const container = this.containerManager.ensure(l.ui.containerID(lock), l.ui.appendContainer(lock)); 24 | const screen = fns.get(l.modeName(lock))(lock); 25 | const props = { 26 | auxiliaryPane: screen.renderAuxiliaryPane(lock), 27 | backHandler: screen.backHandler(lock), 28 | closeHandler: screen.closeHandler(lock), 29 | contentRender: ::screen.render, 30 | footerText: screen.renderFooterText(lock), 31 | headerText: screen.renderHeaderText(lock), 32 | lock: lock, 33 | screenName: screen.name, 34 | submitHandler: screen.submitHandler(lock) 35 | }; 36 | ReactDOM.render(, container); 37 | } else { 38 | let container; 39 | try { 40 | container = this.containerManager.ensure(l.ui.containerID(lock)) 41 | } catch (e) { 42 | // do nothing if container doesn't exist 43 | } 44 | container && ReactDOM.unmountComponentAtNode(container); 45 | } 46 | }); 47 | 48 | const node = global.document.getElementsByTagName("html")[0]; 49 | const className = "auth0-lock-html"; 50 | 51 | const includeClass = locks.some(m => l.rendering(m) && l.ui.appendContainer(m)); 52 | 53 | if (includeClass) { 54 | CSSCore.addClass(node, className); 55 | } else { 56 | CSSCore.removeClass(node, className); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lock/screen.js: -------------------------------------------------------------------------------- 1 | import { closeLock } from './actions'; 2 | import * as l from './index'; 3 | 4 | export default class Screen { 5 | constructor(name) { 6 | this.name = name; 7 | } 8 | 9 | backHandler() { 10 | return null; 11 | } 12 | 13 | closeHandler() { 14 | return closeLock; 15 | } 16 | 17 | escHandler() { 18 | return null; 19 | } 20 | 21 | submitHandler() { 22 | return null; 23 | } 24 | 25 | renderAuxiliaryPane() { 26 | return null; 27 | } 28 | 29 | renderFooterText(lock) { 30 | return this.t(lock, ["footerText"]); 31 | } 32 | 33 | renderHeaderText(lock) { 34 | return this.t(lock, ["headerText"]); 35 | } 36 | 37 | t(lock, keyPath, params) { 38 | return l.ui.t(lock, [this.name].concat(keyPath), params); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lock/signed_in_confirmation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfirmationPane from './confirmation_pane'; 3 | import { closeLock } from './actions'; 4 | import * as l from './index'; 5 | 6 | 7 | export default class SignedInConfirmation extends React.Component { 8 | 9 | t(keyPath, params) { 10 | return l.ui.t(this.props.lock, ["signedIn"].concat(keyPath), params); 11 | } 12 | 13 | handleClose() { 14 | const { closeHandler, lock } = this.props; 15 | closeHandler(l.id(lock)); 16 | } 17 | 18 | render() { 19 | const { lock } = this.props; 20 | const closeHandler = l.ui.closable(lock) ? ::this.handleClose : undefined; 21 | 22 | return ( 23 | 24 |

    {this.t(["success"])}

    25 |
    26 | ); 27 | } 28 | 29 | } 30 | 31 | SignedInConfirmation.propTypes = { 32 | closeHandler: React.PropTypes.func.isRequired, 33 | lock: React.PropTypes.object.isRequired 34 | }; 35 | 36 | export function renderSignedInConfirmation(lock, props = {}) { 37 | props.closeHandler = closeLock; 38 | props.key = "auxiliarypane"; 39 | props.lock = lock; 40 | 41 | return l.signedIn(lock) ? : null; 42 | } 43 | -------------------------------------------------------------------------------- /src/lock/submit_button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Icon from '../icon/icon'; 4 | 5 | export default class SubmitButton extends React.Component { 6 | render() { 7 | const { color, disabled, tabIndex } = this.props; 8 | 9 | return ( 10 | 16 | ); 17 | } 18 | 19 | focus() { 20 | ReactDOM.findDOMNode(this).focus(); 21 | } 22 | } 23 | 24 | SubmitButton.propTypes = { 25 | disabled: React.PropTypes.bool 26 | }; 27 | -------------------------------------------------------------------------------- /src/lock/terms.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Terms extends React.Component { 4 | render() { 5 | return {this.props.children}; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lock/web_api.js: -------------------------------------------------------------------------------- 1 | import Auth0 from 'auth0-js'; 2 | import reqwest from 'reqwest'; 3 | import * as StringUtils from '../utils/string_utils'; 4 | 5 | class Auth0WebAPI { 6 | constructor() { 7 | this.clients = {}; 8 | } 9 | 10 | setupClient(lockID, clientID, domain) { 11 | // TODO: reuse clients 12 | this.clients[lockID] = new Auth0({ 13 | clientID: clientID, 14 | domain: domain, 15 | sendSDKClientInfo: true 16 | }); 17 | } 18 | 19 | signIn(lockID, options, cb) { 20 | const { redirect } = options; 21 | const f = loginCallback(redirect, cb); 22 | const client = this.clients[lockID]; 23 | 24 | transferLoginOptionsToClient(client, options); 25 | 26 | delete options.redirect; 27 | 28 | client.login(options, f); 29 | } 30 | 31 | signOut(lockID, query) { 32 | this.clients[lockID].logout(query); 33 | } 34 | 35 | startPasswordless(lockID, options, cb) { 36 | const client = this.clients[lockID]; 37 | transferLoginOptionsToClient(client, options); 38 | 39 | client.startPasswordless(options, err => cb(normalizeError(err))); 40 | } 41 | 42 | parseHash(lockID, hash) { 43 | return this.clients[lockID].parseHash(hash); 44 | } 45 | 46 | getProfile(lockID, token, callback) { 47 | return this.clients[lockID].getProfile(token, callback); 48 | } 49 | 50 | getUserCountry(lockID, cb) { 51 | // TODO: This code belongs to Auth0.js 52 | const protocol = "https:"; 53 | const domain = this.clients[lockID]._domain; 54 | const endpoint = "/user/geoloc/country"; 55 | const url = joinUrl(protocol, domain, endpoint); 56 | 57 | reqwest({ 58 | url: same_origin(protocol, domain) ? endpoint : url, 59 | method: "get", 60 | type: "json", 61 | crossOrigin: !same_origin(protocol, domain), 62 | success: (res) => cb(null, res.country_code), 63 | error: (err) => cb(err) 64 | }); 65 | } 66 | } 67 | 68 | export default new Auth0WebAPI(); 69 | 70 | function normalizeError(error) { 71 | if (!error) { 72 | return error; 73 | } 74 | 75 | 76 | // TODO: the following checks were copied from https://github.com/auth0/lock/blob/0a5abf1957c9bb746b0710b274d0feed9b399958/index.js#L1263-L1288 77 | // Some of the checks are missing because I couldn't reproduce them and I'm 78 | // affraid they'll break existent functionality if add them. 79 | // We need a better errror handling story in auth0.js. 80 | 81 | if (error.status === "User closed the popup window") { 82 | // { 83 | // status: "User closed the popup window", 84 | // name: undefined, 85 | // code: undefined, 86 | // details: { 87 | // description: "server error", 88 | // code: undefined 89 | // } 90 | // } 91 | return { 92 | error: "lock.popup_closed", 93 | description: "Popup window closed." 94 | }; 95 | } 96 | 97 | if (error.message === 'access_denied' || error.code === "unauthorized") { 98 | // NOTE: couldn't reproduce for error.message === 'access_denied' and can't 99 | // see the difference between the two. 100 | 101 | // { 102 | // code: "unauthorized", 103 | // details: { 104 | // code: "unauthorized" 105 | // error: "unauthorized" 106 | // error_description: "access_denied" 107 | // }, 108 | // name: "unauthorized" 109 | // status: 401 110 | // } 111 | return { 112 | error: "lock.unauthorized", 113 | description: "Permissions were not granted." 114 | } 115 | } 116 | 117 | return { 118 | error: error.details ? error.details.error : (error.statusCode || error.error), 119 | description: error.details ? error.details.error_description : (error.error_description || error.error) 120 | } 121 | } 122 | 123 | // The properties callbackOnLocationHash, callbackURL, and forceJSONP can only 124 | // be specified when counstructing an Auth0 instance. Unfortunately we construct 125 | // the Auth0 client along with the Lock and we don't have the values of those 126 | // options until later when the Lock is shown. While today we may construct the 127 | // client here, in the future that will not be possible becasue we will need to 128 | // retrieve some client information before before we can show the Lock. 129 | function transferLoginOptionsToClient(client, options) { 130 | const { callbackURL, forceJSONP, redirect, responseType } = options; 131 | 132 | client._callbackOnLocationHash = responseType === "token"; 133 | client._callbackURL = callbackURL || client._callbackURL; 134 | client._shouldRedirect = redirect || responseType === "code" || !!callbackURL; 135 | client._useJSONP = forceJSONP; 136 | 137 | delete options.callbackURL; 138 | delete options.forceJSONP; 139 | delete options.responseType; 140 | } 141 | 142 | function loginCallback(redirect, cb) { 143 | if (redirect) { 144 | return error => cb(normalizeError(error)); 145 | } else { 146 | return (error, profile, idToken, accessToken, state, refreshToken) => { 147 | cb(normalizeError(error), profile, idToken, accessToken, state, refreshToken); 148 | } 149 | } 150 | } 151 | 152 | function joinUrl(protocol, domain, endpoint) { 153 | return protocol + '//' + domain + endpoint; 154 | } 155 | 156 | function same_origin(tprotocol, tdomain, tport) { 157 | const protocol = window.location.protocol; 158 | const domain = window.location.hostname; 159 | const port = window.location.port; 160 | tport = tport || ''; 161 | 162 | return protocol === tprotocol && domain === tdomain && port === tport; 163 | } 164 | -------------------------------------------------------------------------------- /src/mode/emailcode/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import AskEmail from '../../passwordless/ask_email'; 3 | import AskEmailVcode from '../../passwordless/ask_email_vcode'; 4 | import * as m from '../../passwordless/index'; 5 | 6 | export default class Emailcode extends Mode { 7 | 8 | constructor() { 9 | super("emailcode"); 10 | } 11 | 12 | willOpen(model, options) { 13 | options.mode.send = "code"; 14 | this.setOptions(options); 15 | } 16 | 17 | render(lock) { 18 | return m.passwordlessStarted(lock) 19 | ? new AskEmailVcode() 20 | : new AskEmail(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/mode/index.js: -------------------------------------------------------------------------------- 1 | import { getEntity, read, setEntity, swap } from '../store/index'; 2 | import { closeLock, openLock } from '../lock/actions'; 3 | import trim from 'trim'; 4 | 5 | export class Mode { 6 | constructor(name) { 7 | this.name = name; 8 | } 9 | 10 | open(id, ...args) { 11 | const { name } = this; 12 | const [options, callback] = openFunctionArgsResolver(name, args); 13 | 14 | warnScopeOpenidProfile(options); 15 | options.signInCallback = callback; 16 | options.mode = {}; 17 | 18 | this.id = id; 19 | this.options = options; // TODO: should clone 20 | const model = read(getEntity, "lock", id); 21 | 22 | this.willOpen(model, options); 23 | 24 | const result = openLock(id, name, this.options); 25 | 26 | delete this.id; 27 | delete this.options; 28 | 29 | return result; 30 | } 31 | 32 | // render must be implemented in each mode 33 | 34 | close(id, force) { 35 | closeLock(id, force); 36 | } 37 | 38 | setModel(m) { 39 | // TODO: unnecessary swap, should pass along the model 40 | swap(setEntity, "lock", this.id, m); 41 | } 42 | 43 | setOptions(options) { 44 | this.options = options; // TODO: should clone 45 | } 46 | 47 | } 48 | 49 | function openFunctionArgsResolver(fnName, args) { 50 | const defaultOptions = {}; 51 | const defaultCallback = () => {}; 52 | 53 | if (args.length == 0) { 54 | return [defaultOptions, defaultCallback]; 55 | } 56 | 57 | if (args.length == 1) { 58 | if (typeof args[0] == "object") { 59 | return [args[0], defaultCallback]; 60 | } else if (typeof args[0] == "function") { 61 | return [defaultOptions, args[0]]; 62 | } else { 63 | throw new Error("When `" + fnName + "` is called with one argument, it must be an `options` object or a `callback` function."); 64 | } 65 | } 66 | 67 | if (args.length == 2) { 68 | if (typeof args[0] != "object") { 69 | throw new Error("When `" + fnName + "` is called with two arguments, an `options` object must be provided as the first argument."); 70 | } 71 | if (typeof args[1] != "function") { 72 | throw new Error("When `" + fnName + "` is called with two arguments, a `callback` function must be provided as the second argument."); 73 | } 74 | return args; 75 | } 76 | 77 | throw new Error("`" + fnName + "` must be called with two arguments at most."); 78 | } 79 | 80 | function warnScopeOpenidProfile(options) { 81 | // TODO: abstract warning output (should receive a message and emit the 82 | // warning unless they are disabled). 83 | const { authParams, disableWarnings } = options; 84 | if (authParams 85 | && typeof authParams === "object" 86 | && trim(authParams.scope || "") === "openid profile" 87 | && !disableWarnings 88 | && console 89 | && console.warn) { 90 | console.warn("Usage of scope 'openid profile' is not recommended. See https://auth0.com/docs/scopes for more details."); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/mode/magiclink/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import MagiclinkScreen from '../../passwordless/magiclink'; 3 | 4 | export default class Magiclink extends Mode { 5 | 6 | constructor() { 7 | super("magiclink"); 8 | } 9 | 10 | willOpen(model, options) { 11 | options.mode.send = "link"; 12 | this.setOptions(options); 13 | } 14 | 15 | render(lock) { 16 | return new MagiclinkScreen(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/mode/sms/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import { setInitialPhoneLocation } from '../../cred/phone-number/actions'; 3 | import AskPhoneNumberVcode from '../../passwordless/ask_phone_number_vcode'; 4 | import AskPhoneNumber from '../../passwordless/ask_phone_number'; 5 | import * as m from '../../passwordless/index'; 6 | 7 | export default class Sms extends Mode { 8 | 9 | constructor() { 10 | super("sms"); 11 | } 12 | 13 | willOpen(model, options) { 14 | this.setModel(setInitialPhoneLocation(model, options)); 15 | options.mode.send = "sms"; 16 | this.setOptions(options); 17 | } 18 | 19 | render(lock) { 20 | return m.passwordlessStarted(lock) 21 | ? new AskPhoneNumberVcode() 22 | : new AskPhoneNumber(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/mode/social-or-emailcode/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import AskEmailVcode from '../../passwordless/ask_email_vcode'; 3 | import AskSocialNetworkOrEmail from '../../cred/or/ask_social_network_or_email'; 4 | import { processSocialOptions } from '../../social/index'; 5 | import * as m from '../../passwordless/index'; 6 | 7 | export default class SocialOrEmailCode extends Mode { 8 | 9 | constructor() { 10 | super("socialOrEmailcode"); 11 | } 12 | 13 | willOpen(model, options) { 14 | options = processSocialOptions(options); 15 | options.mode.send = "code"; 16 | this.setOptions(options); 17 | this.setModel(model.set("forceRedirect", !options.popup)); 18 | } 19 | 20 | render(lock) { 21 | return m.passwordlessStarted(lock) 22 | ? new AskEmailVcode() 23 | : new AskSocialNetworkOrEmail(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/mode/social-or-magiclink/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import AskSocialNetworkOrEmail from '../../cred/or/ask_social_network_or_email'; 3 | import { processSocialOptions } from '../../social/index'; 4 | 5 | export default class SocialOrMagiclink extends Mode { 6 | 7 | constructor() { 8 | super("socialOrMagiclink"); 9 | } 10 | 11 | willOpen(model, options) { 12 | options = processSocialOptions(options); 13 | options.mode.send = "link"; 14 | this.setOptions(options); 15 | this.setModel(model.set("forceRedirect", !options.popup)); 16 | } 17 | 18 | render() { 19 | return new AskSocialNetworkOrEmail(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/mode/social-or-sms/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import { setInitialPhoneLocation } from '../../cred/phone-number/actions'; 3 | import AskSocialNetworkOrPhoneNumber from '../../cred/or/ask_social_network_or_phone_number'; 4 | import AskPhoneNumberVcode from '../../passwordless/ask_phone_number_vcode'; 5 | import { processSocialOptions } from '../../social/index'; 6 | import * as m from '../../passwordless/index'; 7 | 8 | export default class SocialOrSms extends Mode { 9 | 10 | constructor() { 11 | super("socialOrSms"); 12 | } 13 | 14 | willOpen(model, options) { 15 | options = processSocialOptions(options); 16 | model = setInitialPhoneLocation(model, options); 17 | this.setModel(model.set("forceRedirect", !options.popup)); 18 | options.mode.send = "sms"; 19 | this.setOptions(options); 20 | } 21 | 22 | render(lock) { 23 | return m.passwordlessStarted(lock) 24 | ? new AskPhoneNumberVcode() 25 | : new AskSocialNetworkOrPhoneNumber(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/mode/social/spec.js: -------------------------------------------------------------------------------- 1 | import { Mode } from '../index'; 2 | import AskSocialNetwork from '../../cred/social/ask_social_network'; 3 | import { processSocialOptions } from '../../social/index'; 4 | 5 | export default class Social extends Mode { 6 | 7 | constructor() { 8 | super("social"); 9 | } 10 | 11 | willOpen(model, options) { 12 | this.setOptions(processSocialOptions(options)); 13 | this.setModel(model.set("forceRedirect", !options.popup)); 14 | } 15 | 16 | render() { 17 | return new AskSocialNetwork(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/multisize-slide/multisize_slide.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import CSSCore from 'fbjs/lib/CSSCore'; 4 | 5 | export default class Slider extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {children: {current: props.children}}; 9 | } 10 | 11 | componentWillReceiveProps(nextProps) { 12 | if (this.state.children.current.key != nextProps.children.key) { 13 | this.setState({ 14 | children: { 15 | current: nextProps.children, 16 | prev: this.state.children.current 17 | } 18 | }); 19 | this.animate = true; 20 | } else { 21 | this.setState({children: {current: nextProps.children}}); 22 | } 23 | } 24 | 25 | componentDidUpdate() { 26 | if (this.animate) { 27 | this.animate = false; 28 | 29 | const {current, prev} = this.state.children; 30 | const {transitionName} = this.props; 31 | const currentComponent = this.refs[current.key]; 32 | const prevComponent = this.refs[prev.key]; 33 | 34 | const transition = (component, className, delay) => { 35 | const node = ReactDOM.findDOMNode(component); 36 | const activeClassName = `${className}-active`; 37 | 38 | CSSCore.addClass(node, className); 39 | 40 | setTimeout(() => CSSCore.addClass(node, activeClassName), 17); 41 | 42 | if (delay) { 43 | setTimeout(() => { 44 | CSSCore.removeClass(node, className); 45 | CSSCore.removeClass(node, activeClassName); 46 | }, delay); 47 | } 48 | }; 49 | 50 | const callback = (slide) => { 51 | currentComponent.componentWillSlideIn(slide); 52 | const classNamePrefix = slide.reverse ? "reverse-" : ""; 53 | transition(currentComponent, `${classNamePrefix}${transitionName}-enter`, this.props.delay); 54 | transition(prevComponent, `${classNamePrefix}${transitionName}-leave`); 55 | 56 | this.timeout = setTimeout(() => { 57 | this.setState({children: {current: this.state.children.current}}); 58 | currentComponent.componentDidSlideIn(); 59 | this.timeout = null; 60 | }, this.props.delay); 61 | }; 62 | 63 | prevComponent.componentWillSlideOut(callback) 64 | } 65 | } 66 | 67 | componentWillUnmount() { 68 | if (this.timeout) clearTimeout(this.timeout); 69 | } 70 | 71 | render() { 72 | const {current, prev} = this.state.children; 73 | const children = prev ? [current, prev] : [current]; 74 | 75 | const childrenToRender = children.map(child => { 76 | return React.cloneElement(child, {ref: child.key}); 77 | }); 78 | 79 | return React.createElement(this.props.component, {}, childrenToRender); 80 | } 81 | } 82 | 83 | Slider.propTypes = { 84 | component: React.PropTypes.string, 85 | delay: React.PropTypes.number.isRequired, 86 | transitionName: React.PropTypes.string.isRequired, 87 | }; 88 | 89 | Slider.defaultProps = { 90 | component: "span" 91 | }; 92 | -------------------------------------------------------------------------------- /src/passwordless/actions.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { read, getEntity, swap, updateEntity } from '../store/index'; 3 | import { closeLock } from '../lock/actions'; 4 | import webApi from '../lock/web_api'; 5 | import * as c from '../cred/index'; 6 | import * as cs from '../cred/storage'; 7 | import * as l from '../lock/index'; 8 | import * as m from './index'; 9 | 10 | export function requestPasswordlessEmail(id) { 11 | // TODO: abstract this submit thing. 12 | swap(updateEntity, "lock", id, lock => { 13 | if (c.validEmail(lock)) { 14 | return l.setSubmitting(lock, true); 15 | } else { 16 | return c.setShowInvalidEmail(lock); 17 | } 18 | }); 19 | 20 | const lock = read(getEntity, "lock", id); 21 | 22 | if (l.submitting(lock)) { 23 | const isMagicLink = m.send(lock) === "link"; 24 | const options = { 25 | authParams: isMagicLink ? l.login.authParams(lock).toJS() : {}, 26 | callbackURL: l.login.callbackURL(lock), 27 | forceJSONP: l.login.forceJSONP(lock), 28 | email: c.email(lock), 29 | send: m.send(lock), 30 | responseType: l.login.responseType(lock) 31 | }; 32 | 33 | webApi.startPasswordless(id, options, error => { 34 | if (error) { 35 | setTimeout(() => requestPasswordlessEmailError(id, error), 250); 36 | } else { 37 | requestPasswordlessEmailSuccess(id); 38 | } 39 | }); 40 | } 41 | } 42 | 43 | export function requestPasswordlessEmailSuccess(id) { 44 | swap(updateEntity, "lock", id, lock => { 45 | return m.setPasswordlessStarted(l.setSubmitting(lock, false), true); 46 | }); 47 | const lock = read(getEntity, "lock", id); 48 | cs.store(lock, "email", l.modeName(lock)); 49 | if (m.send(lock) === "link") { 50 | l.invokeDoneCallback(lock, null, c.email(lock)); 51 | } 52 | } 53 | 54 | export function requestPasswordlessEmailError(id, error) { 55 | const lock = read(getEntity, "lock", id); 56 | const errorMessage = l.ui.t(lock, ["error", "passwordless", error.error], {medium: "email", __textOnly: true}) || l.ui.t(lock, ["error", "passwordless", "lock.request"], {medium: "email", __textOnly: true}) 57 | swap(updateEntity, "lock", id, l.setSubmitting, false, errorMessage); 58 | if (m.send(lock) === "link") { 59 | l.invokeDoneCallback(lock, error); 60 | } 61 | } 62 | 63 | export function sendSMS(id) { 64 | // TODO: abstract this submit thing. 65 | swap(updateEntity, "lock", id, lock => { 66 | 67 | if (c.validPhoneNumber(lock)) { 68 | return l.setSubmitting(lock, true); 69 | } else { 70 | return c.setShowInvalidPhoneNumber(lock, true); 71 | } 72 | }); 73 | 74 | const lock = read(getEntity, "lock", id); 75 | 76 | if (l.submitting(lock)) { 77 | const options = {phoneNumber: c.fullPhoneNumber(lock)}; 78 | webApi.startPasswordless(id, options, error => { 79 | if (error) { 80 | setTimeout(() => sendSMSError(id, error), 250); 81 | } else { 82 | sendSMSSuccess(id); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | export function sendSMSSuccess(id) { 89 | swap(updateEntity, "lock", id, lock => { 90 | lock = l.setSubmitting(lock, false); 91 | lock = m.setPasswordlessStarted(lock, true); 92 | return lock; 93 | }); 94 | 95 | const lock = read(getEntity, "lock", id); 96 | cs.store(lock, "phoneNumber", l.modeName(lock)); 97 | } 98 | 99 | export function sendSMSError(id, error) { 100 | const lock = read(getEntity, "lock", id); 101 | let errorMessage; 102 | if (error.error === "sms_provider_error" && (error.description || "").indexOf("(Code: 21211)") > -1) { 103 | errorMessage = l.ui.t(lock, ["error", "passwordless", "sms_provider_error.bad_phone_number"], {phoneNumber: c.fullPhoneNumber(lock), __textOnly: true}); 104 | } else { 105 | errorMessage = l.ui.t(lock, ["error", "passwordless", error.error], {medium: "SMS", __textOnly: true}) || l.ui.t(lock, ["error", "passwordless", "lock.request"], {medium: "SMS", __textOnly: true}) 106 | } 107 | 108 | swap(updateEntity, "lock", id, l.setSubmitting, false, errorMessage); 109 | } 110 | 111 | export function resendEmail(id) { 112 | swap(updateEntity, "lock", id, m.resend); 113 | 114 | const lock = read(getEntity, "lock", id); 115 | const options = { 116 | authParams: m.send(lock) === "link" ? l.login.authParams(lock).toJS() : {}, 117 | email: c.email(lock), 118 | send: m.send(lock), 119 | responseType: l.login.responseType(lock), 120 | callbackURL: l.login.callbackURL(lock), 121 | forceJSONP: l.login.forceJSONP(lock) 122 | }; 123 | webApi.startPasswordless(id, options, error => { 124 | if (error) { 125 | resendEmailError(id, error); 126 | } else { 127 | resendEmailSuccess(id); 128 | } 129 | }); 130 | } 131 | 132 | export function resendEmailSuccess(id) { 133 | swap(updateEntity, "lock", id, m.setResendSuccess); 134 | const lock = read(getEntity, "lock", id); 135 | l.invokeDoneCallback(lock, null, c.email(lock)); 136 | } 137 | 138 | export function resendEmailError(id, error) { 139 | swap(updateEntity, "lock", id, m.setResendFailed); 140 | const lock = read(getEntity, "lock", id); 141 | l.invokeDoneCallback(lock, error); 142 | } 143 | 144 | export function signIn(id) { 145 | // TODO: abstract this submit thing 146 | swap(updateEntity, "lock", id, lock => { 147 | if (c.validVcode(lock)) { 148 | return l.setSubmitting(lock, true); 149 | } else { 150 | return c.setShowInvalidVcode(lock); 151 | } 152 | }); 153 | 154 | const lock = read(getEntity, "lock", id); 155 | 156 | if (l.submitting(lock)) { 157 | const options = { 158 | passcode: c.vcode(lock), 159 | redirect: l.shouldRedirect(lock), 160 | responseType: l.login.responseType(lock), 161 | callbackURL: l.login.callbackURL(lock), 162 | forceJSONP: l.login.forceJSONP(lock) 163 | }; 164 | 165 | if (m.send(lock) === "sms") { 166 | options.phoneNumber = c.fullPhoneNumber(lock); 167 | } else { 168 | options.email = c.email(lock); 169 | } 170 | 171 | webApi.signIn( 172 | id, 173 | Map(options).merge(l.login.authParams(lock)).toJS(), 174 | (error, ...args) => { 175 | if (error) { 176 | setTimeout(() => signInError(id, error), 250); 177 | } else { 178 | signInSuccess(id, ...args); 179 | } 180 | } 181 | ); 182 | } 183 | } 184 | 185 | function signInSuccess(id, ...args) { 186 | const lock = read(getEntity, "lock", id); 187 | const autoclose = l.ui.autoclose(lock); 188 | 189 | if (!autoclose) { 190 | swap(updateEntity, "lock", id, lock => l.setSignedIn(l.setSubmitting(lock, false), true)); 191 | l.invokeDoneCallback(lock, null, ...args); 192 | } else { 193 | closeLock(id, false, lock => l.invokeDoneCallback(lock, null, ...args)); 194 | } 195 | } 196 | 197 | function signInError(id, error) { 198 | const lock = read(getEntity, "lock", id); 199 | const cred = m.send(lock) === "sms" ? "phone number" : "email"; 200 | const errorMessage = l.ui.t(lock, ["error", "signIn", error.error], {cred: cred, __textOnly: true}) || l.ui.t(lock, ["error", "signIn", "lock.request"], {cred: cred, __textOnly: true}); 201 | swap(updateEntity, "lock", id, l.setSubmitting, false, errorMessage); 202 | 203 | l.invokeDoneCallback(lock, error); 204 | } 205 | 206 | export function reset(id, opts = {}) { 207 | swap(updateEntity, "lock", id, m.reset, opts); 208 | } 209 | 210 | export function back(id, resetOpts = {}) { 211 | reset(id, resetOpts); 212 | } 213 | -------------------------------------------------------------------------------- /src/passwordless/ask_email.jsx: -------------------------------------------------------------------------------- 1 | import Base from '../cred/email/ask_email'; 2 | import { requestPasswordlessEmail } from './actions'; 3 | 4 | export default class AskEmail extends Base { 5 | 6 | submitHandler() { 7 | return requestPasswordlessEmail; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/passwordless/ask_email_vcode.jsx: -------------------------------------------------------------------------------- 1 | import Base from './ask_vcode'; 2 | import * as c from '../cred/index'; 3 | 4 | export default class AskEmailVcode extends Base { 5 | 6 | renderHeaderText(lock) { 7 | return this.t(lock, ["headerText"], {email: c.email(lock)}); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/passwordless/ask_phone_number.jsx: -------------------------------------------------------------------------------- 1 | import Base from '../cred/phone-number/ask_phone_number'; 2 | import { sendSMS } from './actions'; 3 | 4 | export default class AskPhoneNumber extends Base { 5 | 6 | submitHandler() { 7 | return sendSMS; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/passwordless/ask_phone_number_vcode.jsx: -------------------------------------------------------------------------------- 1 | import Base from './ask_vcode'; 2 | import * as c from '../cred/index'; 3 | 4 | export default class AskPhoneNumberVcode extends Base { 5 | 6 | renderHeaderText(lock) { 7 | return this.t(lock, ["headerText"], {phoneNumber: c.fullHumanPhoneNumber(lock)}); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/passwordless/ask_vcode.jsx: -------------------------------------------------------------------------------- 1 | import Base from '../cred/vcode/ask_vcode'; 2 | import { back, signIn } from './actions'; 3 | import { renderSignedInConfirmation } from '../lock/signed_in_confirmation'; 4 | 5 | export default class AskVcode extends Base { 6 | 7 | backHandler() { 8 | return (id) => back(id, {clearCred: ["vcode"]}); 9 | } 10 | 11 | submitHandler() { 12 | return signIn; 13 | } 14 | 15 | renderAuxiliaryPane(lock) { 16 | return renderSignedInConfirmation(lock); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/passwordless/email_sent_confirmation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfirmationPane from '../lock/confirmation_pane'; 3 | import Icon from '../icon/icon'; 4 | import { closeLock } from '../lock/actions'; 5 | import * as l from '../lock/index'; 6 | import * as c from '../cred/index'; 7 | 8 | import { resendEmail, reset } from './actions'; 9 | import * as m from './index'; 10 | 11 | 12 | class ResendLink extends React.Component { 13 | render() { 14 | const { label, onClick } = this.props; 15 | return( 16 | 17 | {label} 18 | 19 | ); 20 | } 21 | } 22 | 23 | class Resend extends React.Component { 24 | render() { 25 | const { labels, lock } = this.props; 26 | 27 | const resendLink = m.resendAvailable(lock) && 28 | ; 30 | 31 | const resendingLabel = m.resendOngoing(lock) && 32 | {labels.resending}; 33 | 34 | const resendSuccessLabel = m.resendSuccess(lock) && 35 | {labels.sent}; 36 | 37 | const resendFailedLabel = m.resendFailed(lock) && 38 | {labels.failed}; 39 | 40 | return ( 41 | 42 | {resendLink} 43 | {resendingLabel} 44 | {resendSuccessLabel} 45 | {resendFailedLabel} 46 | 47 | ); 48 | } 49 | 50 | handleClick(e) { 51 | e.preventDefault(); 52 | resendEmail(l.id(this.props.lock)); 53 | } 54 | 55 | } 56 | 57 | export default class EmailSentConfirmation extends React.Component { 58 | render() { 59 | const { lock } = this.props; 60 | const closeHandler = l.ui.closable(lock) ? ::this.handleClose : undefined; 61 | const labels = { 62 | failed: this.t(["failedLabel"]), 63 | resend: this.t(["resendLabel"]), 64 | resending: this.t(["resendingLabel"]), 65 | retry: this.t(["retryLabel"]), 66 | sent: this.t(["sentLabel"]) 67 | }; 68 | 69 | return ( 70 | 71 |

    {this.t(["success"], {email: c.email(lock)})}

    72 | 73 |
    74 | ) 75 | } 76 | 77 | handleBack() { 78 | reset(l.id(this.props.lock), {clearCred: []}); 79 | } 80 | 81 | handleClose() { 82 | closeLock(l.id(this.props.lock)); 83 | } 84 | 85 | t(keyPath, params) { 86 | return l.ui.t(this.props.lock, ["emailSent"].concat(keyPath), params); 87 | } 88 | 89 | } 90 | 91 | export function renderEmailSentConfirmation(lock, props = {}) { 92 | props.key = "auxiliarypane"; 93 | props.lock = lock; 94 | 95 | return m.passwordlessStarted(lock) 96 | ? 97 | : null; 98 | } 99 | -------------------------------------------------------------------------------- /src/passwordless/index.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import * as l from '../lock/index'; 3 | 4 | function setResendStatus(m, value) { 5 | // TODO: check value 6 | return m.set("resendStatus", value); 7 | } 8 | 9 | export function setResendSuccess(m) { 10 | return setResendStatus(m, "success"); 11 | } 12 | 13 | export function resendSuccess(m) { 14 | return resendStatus(m) == "success"; 15 | } 16 | 17 | export function setResendFailed(m) { 18 | return setResendStatus(m, "failed"); 19 | } 20 | 21 | export function resendFailed(m) { 22 | return resendStatus(m) == "failed"; 23 | } 24 | 25 | export function resendOngoing(m) { 26 | return resendStatus(m) == "ongoing"; 27 | } 28 | 29 | export function resend(m) { 30 | if (resendAvailable(m)) { 31 | return setResendStatus(m, "ongoing"); 32 | } else { 33 | return m; 34 | } 35 | } 36 | 37 | function resendStatus(m) { 38 | return m.get("resendStatus", "waiting"); 39 | } 40 | 41 | export function resendAvailable(m) { 42 | return resendStatus(m) == "waiting" || resendStatus(m) == "failed"; 43 | } 44 | 45 | export function reset(m, opts = {}) { 46 | let keyPaths = List([ 47 | ["passwordlessStarted"], 48 | ["resendStatus"], 49 | ["selectingLocation"], 50 | ["signedIn"] 51 | ]); 52 | 53 | // TODO `signedIn` should be handled at the lock level, later instead of 54 | // calling l.clearGlobalError we should call something like l.reset. 55 | 56 | const { clearCred } = opts; 57 | 58 | if (!clearCred) { 59 | keyPaths = keyPaths.push(["cred"]); 60 | } else { 61 | const credKeyPaths = List(clearCred).map(x => ["cred", x]); 62 | keyPaths = keyPaths.concat(credKeyPaths); 63 | } 64 | 65 | m = keyPaths.reduce((r, v) => r.removeIn(v), m); 66 | 67 | return l.clearGlobalError(m); 68 | } 69 | 70 | export function send(m) { 71 | return l.modeOptions(m).get("send", "link") 72 | } 73 | 74 | export function isSendLink(m) { 75 | return send(m) === "link"; 76 | } 77 | 78 | export function setPasswordlessStarted(m, value) { 79 | return m.set("passwordlessStarted", value); 80 | } 81 | 82 | export function passwordlessStarted(m) { 83 | return m.get("passwordlessStarted", false); 84 | } 85 | -------------------------------------------------------------------------------- /src/passwordless/magiclink.jsx: -------------------------------------------------------------------------------- 1 | import Base from './ask_email'; 2 | import { renderEmailSentConfirmation } from './email_sent_confirmation'; 3 | 4 | export default class Magiclink extends Base { 5 | 6 | renderAuxiliaryPane(lock) { 7 | return renderEmailSentConfirmation(lock); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/preload/index.js: -------------------------------------------------------------------------------- 1 | export function img(src, cb) { 2 | const img = document.createElement("img"); 3 | img.addEventListener("load", () => { cb(null, img); }); 4 | img.addEventListener("error", (event) => { cb(event) }); 5 | img.src = src; 6 | return img; 7 | } 8 | -------------------------------------------------------------------------------- /src/social/actions.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { getEntity, read, swap, updateEntity } from '../store/index'; 3 | import { closeLock } from '../lock/actions'; 4 | import WebAPI from '../lock/web_api'; 5 | import * as l from '../lock/index'; 6 | 7 | export function signIn(id, connection) { 8 | swap(updateEntity, "lock", id, l.setSubmitting, true); 9 | 10 | const lock = read(getEntity, "lock", id); 11 | 12 | const options = l.login.authParams(lock).merge(Map({ 13 | connection: connection.name, 14 | popup: l.ui.popup(lock), 15 | popupOptions: l.ui.popupOptions(lock), 16 | redirect: !l.ui.popup(lock), 17 | responseType: l.login.responseType(lock), 18 | callbackURL: l.login.callbackURL(lock), 19 | forceJSONP: l.login.forceJSONP(lock) 20 | })).toJS(); 21 | 22 | if (l.ui.popup(lock) && connection.strategy === "facebook") { 23 | options.display = "popup"; 24 | } 25 | 26 | WebAPI.signIn(id, options, (error, ...args) => { 27 | if (error) { 28 | setTimeout(() => signInError(id, error), 250); 29 | } else { 30 | signInSuccess(id, ...args); 31 | } 32 | }); 33 | } 34 | 35 | function signInSuccess(id, ...args) { 36 | const lock = read(getEntity, "lock", id); 37 | const autoclose = l.ui.autoclose(lock); 38 | 39 | if (!autoclose) { 40 | swap(updateEntity, "lock", id, lock => l.setSignedIn(l.setSubmitting(lock, false), true)); 41 | l.invokeDoneCallback(lock, null, ...args); 42 | } else { 43 | closeLock(id, false, lock => l.invokeDoneCallback(lock, null, ...args)); 44 | } 45 | } 46 | 47 | function signInError(id, error) { 48 | const lock = read(getEntity, "lock", id); 49 | const errorMessage = l.ui.t(lock, ["error", "signIn", error.error], {__textOnly: true}) || l.ui.t(lock, ["error", "signIn", "lock.request"], {__textOnly: true}); 50 | swap(updateEntity, "lock", id, l.setSubmitting, false, errorMessage); 51 | 52 | l.invokeDoneCallback(lock, error); 53 | } 54 | -------------------------------------------------------------------------------- /src/social/index.js: -------------------------------------------------------------------------------- 1 | import * as l from '../lock/index'; 2 | 3 | export const STRATEGIES = { 4 | "amazon": "Amazon", 5 | "aol": "Aol", 6 | "baidu": "百度", 7 | "box": "Box", 8 | "dwolla": "Dwolla", 9 | "ebay": "ebay", 10 | "exact": "Exact", 11 | "facebook": "Facebook", 12 | "fitbit": "Fitbit", 13 | "github": "GitHub", 14 | "google-openid": "Google OpenId", 15 | "google-oauth2": "Google", 16 | "instagram": "Instagram", 17 | "linkedin": "LinkedIn", 18 | "miicard": "miiCard", 19 | "paypal": "PayPal", 20 | "planningcenter": "Planning Center", 21 | "renren": "人人", 22 | "salesforce": "Salesforce", 23 | "salesforce-community": "Salesforce Community", 24 | "salesforce-sandbox": "Salesforce (sandbox)", 25 | "shopify": "Shopify", 26 | "soundcloud": "Soundcloud", 27 | "thecity": "The City", 28 | "thecity-sandbox": "The City (sandbox)", 29 | "thirtysevensignals": "37 Signals", 30 | "twitter": "Twitter", 31 | "vkontakte": "vKontakte", 32 | "windowslive": "Microsoft Account", 33 | "wordpress": "Wordpress", 34 | "yahoo": "Yahoo!", 35 | "yammer": "Yammer", 36 | "yandex": "Yandex", 37 | "weibo": "新浪微博" 38 | }; 39 | 40 | export function displayName(connection) { 41 | return STRATEGIES[connection.strategy]; 42 | } 43 | 44 | export function validateSocialOptions(options) { 45 | const { connections } = options; 46 | if (!Array.isArray(connections) || connections.length === 0) { 47 | throw new Error("The `connections` option array needs to be provided with at least one connection."); 48 | } 49 | 50 | connections.forEach(x => { 51 | if (typeof x === "string") { 52 | if (!STRATEGIES[x]) { 53 | throw new Error(`An unknown "${x}" connection was provided.`); 54 | } 55 | } else if (typeof x === "object" && typeof x.name === "string" && typeof x.strategy === "string") { 56 | if (!STRATEGIES[x.strategy]) { 57 | throw new Error(`A connection with an unknown "${x.strategy}" strategy was provided.`); 58 | } 59 | } else { 60 | throw new Error("A connection with an invalid format was provided. It must be a string or an object with name and strategy properties."); 61 | } 62 | }); 63 | } 64 | 65 | export function processSocialOptions(options) { 66 | validateSocialOptions(options); 67 | 68 | const { connections, socialBigButtons } = options; 69 | 70 | options.connections = connections.map(x => ( 71 | typeof x === "string" ? {name: x, strategy: x} : x 72 | )); 73 | 74 | options.mode.socialBigButtons = socialBigButtons === undefined 75 | ? connections.length <= 3 76 | : socialBigButtons; 77 | 78 | return options; 79 | } 80 | 81 | export function useBigButtons(m) { 82 | return l.modeOptions(m).get("socialBigButtons", true); 83 | } 84 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import atom from '../atom/index'; 2 | import { Map } from 'immutable'; 3 | 4 | const store = atom(new Map({})); 5 | 6 | export function subscribe(key, f) { 7 | store.addWatch(key, f); 8 | } 9 | 10 | export function unsubscribe(key) { 11 | store.removeWatch(key); 12 | } 13 | 14 | export function swap(...args) { 15 | return store.swap(...args); 16 | } 17 | 18 | export function updateEntity(state, coll, id, f, ...args) { 19 | return state.updateIn([coll, id], new Map({}), x => f(x, ...args)); 20 | } 21 | 22 | export function setEntity(state, coll, id, m) { 23 | if (m === undefined) { 24 | m = id; 25 | id = 0; 26 | } 27 | 28 | return state.setIn([coll, id], m); 29 | } 30 | 31 | export function read(f, ...args) { 32 | return f(store.deref(), ...args); 33 | } 34 | 35 | export function getEntity(state, coll, id = 0) { 36 | return state.getIn([coll, id]); 37 | } 38 | 39 | export function getCollection(state, coll) { 40 | return state.get(coll); 41 | } 42 | 43 | export function removeEntity(state, coll, id = 0) { 44 | return state.removeIn([coll, id]); 45 | } 46 | 47 | // function updateCollection(coll, f, ...args) { 48 | // store.swap(state => state.update(coll, xs => f(xs, ...args))); 49 | // } 50 | // 51 | // function updateFilteredCollection(coll, pred, f, ...args) { 52 | // updateCollection(coll, xs => xs.merge(xs.filter(pred).map(x => f(x, ...args)))); 53 | // } 54 | 55 | export function getState() { 56 | return store.deref(); 57 | } 58 | 59 | // DEV 60 | // store.addWatch("keepHistory", (key, oldState, newState) => { 61 | // if (!global.window.h) global.window.h = []; global.window.h.push(newState); 62 | // console.debug("something changed", newState.toJS()); 63 | // }); 64 | -------------------------------------------------------------------------------- /src/utils/esc_keydown_utils.js: -------------------------------------------------------------------------------- 1 | export default class EscKeydownUtils { 2 | constructor(f) { 3 | this.handler = (e) => { 4 | if (e.keyCode == 27 && e.target.tagName != "INPUT") { 5 | f(); 6 | } 7 | }; 8 | global.document.addEventListener('keydown', this.handler, false); 9 | } 10 | 11 | release() { 12 | global.document.removeEventListener('keydown', this.handler); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/fn_utils.js: -------------------------------------------------------------------------------- 1 | export function debounce(f, delay) { 2 | let t; 3 | return function(...args) { 4 | function handler() { 5 | clearTimeout(t); 6 | f.apply(undefined, args); 7 | } 8 | clearTimeout(t); 9 | t = setTimeout(handler, delay); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/id_utils.js: -------------------------------------------------------------------------------- 1 | export function random() { 2 | return (+new Date() + Math.floor(Math.random() * 10000000)).toString(36); 3 | } 4 | 5 | let start = 1; 6 | export function incremental() { 7 | return start++; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/jsonp_utils.js: -------------------------------------------------------------------------------- 1 | import jsonp from 'jsonp'; 2 | 3 | class JSONPUtils { 4 | get(...args) { 5 | return jsonp(...args); 6 | } 7 | } 8 | 9 | export default new JSONPUtils(); 10 | -------------------------------------------------------------------------------- /src/utils/media_utils.js: -------------------------------------------------------------------------------- 1 | export function isSmallScreen() { 2 | return window.matchMedia && !window.matchMedia("(min-width: 380px)").matches; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/string_utils.js: -------------------------------------------------------------------------------- 1 | export function matches(search, str) { 2 | return str.toLowerCase().indexOf(search.toLowerCase()) > -1; 3 | } 4 | 5 | export function startsWith(str, search) { 6 | return str.indexOf(search) === 0; 7 | } 8 | -------------------------------------------------------------------------------- /support/design/control.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Immutable from 'immutable'; 3 | import atom from '../../src/atom/index'; 4 | import { getState, subscribe, swap, unsubscribe } from '../../src/store/index'; 5 | 6 | export const store = atom(Immutable.fromJS({ 7 | latency: 2500, 8 | signIn: {response: "success"}, 9 | startPasswordless: {response: "success"} 10 | })); 11 | 12 | export default class Control extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.handleKeypress = ::this.handleKeypress; 16 | this.state = store.deref().toJS(); 17 | this.state.showing = false; 18 | } 19 | 20 | componentDidMount() { 21 | document.addEventListener("keypress", this.handleKeypress, false); 22 | store.addWatch("control", (key, oldState, newState) => this.setState(newState.toJS())); 23 | } 24 | 25 | componentWillUnmount() { 26 | document.removeEventListener("keypress", this.handleKeypress); 27 | store.removeWatch("control"); 28 | } 29 | 30 | render() { 31 | if (!this.state.showing) return null; 32 | 33 | return ( 34 |
    35 |
    36 |

    startPasswordless

    37 | 39 | 40 |

    signIn

    41 | 43 | 44 |

    Latency

    45 | 46 |
    47 |
    48 |

    49 | close 50 |
    51 |
    52 | ); 53 | } 54 | 55 | handleCloseClick(e) { 56 | e.preventDefault(); 57 | this.setState({showing: false}); 58 | } 59 | 60 | handleKeypress(e) { 61 | if (e.which === 63) { 62 | this.setState({showing: true}); 63 | } 64 | } 65 | 66 | handleStartPasswodlessResponseChange(value) { 67 | store.swap(state => state.setIn(["startPasswordless", "response"], value)); 68 | } 69 | 70 | handleSignInResponseChange(value) { 71 | store.swap(state => state.setIn(["signIn", "response"], value)); 72 | } 73 | 74 | handleLatencyChange(e) { 75 | store.swap(state => state.set("latency", e.target.value)); 76 | } 77 | } 78 | 79 | class ResultSelect extends React.Component { 80 | render() { 81 | const { selected, onChange } = this.props; 82 | const options = ["success", "error"].map(x => { 83 | return ; 84 | }); 85 | 86 | return ( 87 |
    88 | Response status: { } 89 | 90 |
    91 | ); 92 | } 93 | 94 | handleChange(e) { 95 | e.preventDefault(); 96 | this.props.onChange(e.target.value); 97 | } 98 | } 99 | 100 | class Snapshot extends React.Component { 101 | constructor(props) { 102 | super(props); 103 | this.state = {snapshot: global.JSON.stringify(getState().toJS())}; 104 | } 105 | 106 | componentDidMount() { 107 | subscribe("design-tools.snapshot", (key, oldState, newState) => { 108 | if (newState) { 109 | this.setState({snapshot: global.JSON.stringify(newState.toJS())}); 110 | } 111 | }); 112 | } 113 | 114 | componentWillUnmount() { 115 | unsubscribe("design-tools.snapshot"); 116 | } 117 | 118 | render() { 119 | return ( 120 |
    121 |

    Snapshot

    122 | State: { } 123 |