├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── form.js ├── index.js └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Luke Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperapp-firebase-auth 2 | 3 | > Drop in firebase authentication for hyperapps 4 | 5 | ![ezgif com-gif-maker](https://user-images.githubusercontent.com/1457604/29901861-6cf8c5d4-8df2-11e7-9611-e2800e7bde96.gif) 6 | 7 | This project exports a [hyperapp](https://github.com/hyperapp/hyperapp) module (state/actions/view) that wraps the [Firebase authentication API](https://firebase.google.com/docs/auth/). It manages the application state and renders appropriate views for the authentication flows `Sign In` and `Create User`. 8 | 9 | **DEMO:** https://codepen.io/lukejacksonn/pen/xLBJoN 10 | 11 | Out of the box features include: 12 | 13 | - No server setup or backend code 14 | - Human readable error messages 15 | - Email validation and confirmation 16 | - Reset password by email 17 | 18 | ## Usage 19 | 20 | > If you have not already initialized firebase on your frontend see [Firebase Setup](#firebase-setup) 21 | 22 | Install the package from npm or include from [CDN](https://unpkg.com/hyperapp-firebase-auth): 23 | 24 | ``` 25 | npm i hyperapp-firebase-auth 26 | ``` 27 | 28 | Import the module `firebaseAuth` and the `FirebaseAuthDialog` view: 29 | 30 | ```js 31 | import { app, h } from 'hyperapp' 32 | import { firebaseAuth, FirebaseAuthDialog } from 'hyperapp-firebase-auth' 33 | 34 | const main = 35 | app( 36 | { auth: firebaseAuth.state }, 37 | { auth: firebaseAuth.actions }, 38 | (state, actions) => 39 | h('main', {}, [ 40 | // Only shows when NOT authenticated 41 | FirebaseAuthDialog(state.auth, actions.auth), 42 | // Only shows when authenticated 43 | state.auth.authed && `Hello ${state.auth.user.uid}!` 44 | ]), 45 | document.body 46 | ) 47 | 48 | firebase.auth().onAuthStateChanged(main.auth.userChanged) 49 | ``` 50 | 51 | 52 | ## How it works 53 | 54 | - An empty element is rendered until an auth status is received from Firebase 55 | - If the auth status is null then the user is prompted to enter their email address 56 | - Existing users are then prompted to enter their password to sign in 57 | - New users are prompted to confirm their email address and set a password to sign up 58 | - The dialog will not be rendered once auth status returns a valid Firebase user 59 | 60 | 61 | ## Firebase Setup 62 | 63 | If you want to use Firebase Authentication for your own apps then you will need to create a Firebase Account and create a new project. This can be done for **free** at https://console.firebase.google.com. 64 | 65 | > If you don't want to create your own project then you can use the example config below 66 | 67 | Once you have created a project you will be presented with an app configuration like this: 68 | 69 | ```html 70 | 71 | 82 | ``` 83 | 84 | Add this snippet to your `index.html` **before** any of your hyperapp application code. This ensures that the Firebase API exists when the module loads. Once you are setup, visit the link below (replacing `${projectId}` with the projectId from your newly created Firebase project config) and `Enable` the `Email/Password` provider. 85 | 86 | ``` 87 | https://console.firebase.google.com/project/${projectId}/authentication/providers 88 | ``` 89 | 90 | That is all the back and front end configuration you need to do. 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperapp-firebase-auth", 3 | "version": "0.3.0", 4 | "description": "A drop in authentication solution for hyperapps using firebase", 5 | "main": "dist/index.js", 6 | "jsnext:main": "src/index.js", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "rollup -i src/index.js -o dist/index.js -m -g hyperapp:hyperapp -f umd -n HyperappFirebaseAuth", 13 | "prepublish": "npm run build" 14 | }, 15 | "dependencies": { 16 | "hyperapp": "^1.0.3", 17 | "rollup": "^0.49.1" 18 | }, 19 | "peerDependencies": { 20 | "hyperapp": "^1.0.3" 21 | }, 22 | "keywords": [ 23 | "hyperapp", 24 | "firebase", 25 | "authentication", 26 | "mixin", 27 | "ui" 28 | ], 29 | "author": "Luke Jackson", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /src/form.js: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | 3 | const parseFormInputs = form => 4 | Array.from(form.elements) 5 | .filter(input => input.type !== 'submit') 6 | .reduce((a,{ name, value }) => Object.assign(a, { [name]: value }), {}) 7 | 8 | export default ({ 9 | key = 'form-' + Math.floor(Math.random()*100), 10 | titleText = 'Untitled Form', 11 | promptText = 'Please fill out and submit the form below', 12 | submitText = 'Submit', 13 | inputs = [{ name: 'lorem', type: 'text', placeholder: 'Example Input' }], 14 | action = data => console.log('Submitted', data), 15 | errorText = '', 16 | links = [] 17 | }) => 18 | h('form', { 19 | key, 20 | oncreate: e => e.elements[0].focus(), 21 | onsubmit: e => { 22 | e.preventDefault() 23 | return action(parseFormInputs(e.target)) 24 | } 25 | },[ 26 | h('header', {}, [ 27 | h('h3', {}, titleText), 28 | h('p', {}, promptText), 29 | ]), 30 | inputs.map(props => h('label', { style: { display: props.type === 'hidden' ? 'none' : false }}, h('input', props))), 31 | errorText && h('error-', {}, errorText), 32 | h('button', { type: 'submit' }, submitText), 33 | h('footer', {}, links.map(x => 34 | h('a', { href: '#', onclick: e => e.preventDefault() || x.action() }, x.text) 35 | )), 36 | ]) 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | import Form from './form' 3 | 4 | const auth = firebase.auth() 5 | 6 | const Identity = (state, actions) => 7 | Form({ 8 | key: 'identity-form', 9 | titleText: 'Identity Required', 10 | promptText: `New and existing users please enter your email address to continue`, 11 | submitText: 'Continue', 12 | errorText: state.error.message, 13 | inputs: [ 14 | { 15 | name: 'email', 16 | type: 'email', 17 | placeholder: 'Email Address', 18 | autocomplete: 'email', 19 | }, 20 | ], 21 | action: actions.fetchProviders, 22 | }) 23 | 24 | const Signin = (state, actions) => 25 | Form({ 26 | key: 'signin-form', 27 | titleText: 'Existing Identity', 28 | promptText: 29 | 'In order for us to confirm your identity please enter your password.', 30 | submitText: 'Sign In', 31 | errorText: state.error.message, 32 | inputs: [ 33 | { name: 'password', type: 'password', placeholder: 'Password' }, 34 | { name: 'email', type: 'hidden', value: state.user.email }, 35 | ], 36 | action: actions.signin, 37 | links: [ 38 | { 39 | text: 'Sign in with a different identity', 40 | action: actions.resetIdentity, 41 | }, 42 | { text: 'Reset password', action: actions.resetPassword }, 43 | ], 44 | }) 45 | 46 | const Signup = (state, actions) => 47 | Form({ 48 | key: 'signup-form', 49 | titleText: 'New Identity', 50 | promptText: 51 | 'Please confirm your email address and a set a secure password.', 52 | submitText: 'Create User', 53 | errorText: state.error.message, 54 | inputs: [ 55 | { name: 'email', type: 'email', value: state.user.email }, 56 | { name: 'password', type: 'password', placeholder: 'Password' }, 57 | ], 58 | action: actions.signup, 59 | links: [ 60 | { 61 | text: 'Sign in with a different identity', 62 | action: actions.resetIdentity, 63 | }, 64 | ], 65 | }) 66 | 67 | export const FirebaseAuthDialog = (state, actions) => 68 | state.checked && 69 | !state.authed && 70 | h( 71 | 'dialog', 72 | { 73 | key: 'firebase-auth-dialog', 74 | }, 75 | [ 76 | !state.user.email 77 | ? Identity(state, actions) 78 | : state.hasIdentity.length 79 | ? Signin(state, actions) 80 | : Signup(state, actions), 81 | ] 82 | ) 83 | 84 | export const firebaseAuth = { 85 | state: { 86 | authed: false, 87 | checked: false, 88 | user: null, 89 | error: {}, 90 | hasIdentity: [], 91 | }, 92 | actions: { 93 | setHasIdentity: hasIdentity => ({ hasIdentity }), 94 | setUser: user => ({ user }), 95 | setError: (error = {}) => ({ error }), 96 | userChanged: user => ({ 97 | user: user || {}, 98 | authed: !!user, 99 | checked: true, 100 | }), 101 | resetIdentity: () => ({ 102 | user: {}, 103 | error: {}, 104 | }), 105 | signout: () => { 106 | auth.signOut() 107 | }, 108 | signin: ({ email, password }) => (_, { setError }) => { 109 | setError() 110 | auth.signInWithEmailAndPassword(email, password).catch(setError) 111 | }, 112 | signup: ({ email, password }) => (_, { setError, setUser }) => { 113 | setError() 114 | setUser({ email }) 115 | auth.createUserWithEmailAndPassword(email, password).catch(setError) 116 | }, 117 | fetchProviders: ({ email }) => ( 118 | _, 119 | { setError, setUser, setHasIdentity } 120 | ) => { 121 | setError() 122 | auth 123 | .fetchProvidersForEmail(email) 124 | .then(providers => { 125 | setUser({ email }) 126 | setHasIdentity(providers) 127 | }) 128 | .catch(setError) 129 | }, 130 | resetPassword: ({ email }) => (_, { setError }) => 131 | confirm(`Send a password reset email to ${email}?`) && 132 | auth 133 | .sendPasswordResetEmail(email) 134 | .then(_ => 135 | alert( 136 | `A password reset email has been sent to ${email} please check your inbox.` 137 | ) 138 | ) 139 | .catch(setError), 140 | }, 141 | } 142 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | dialog { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | } 6 | 7 | form { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: flex-start; 11 | width: 16rem; 12 | margin: auto; 13 | *+* { margin-top: 1rem; } 14 | 15 | header { 16 | p { 17 | font-size: .8rem; 18 | color: rgba(0,0,0,.5); 19 | } 20 | } 21 | 22 | footer { 23 | > *+* { margin-top: 0; } 24 | &:empty { display: none; } 25 | a { 26 | font-size: .62rem; 27 | color: #1e88e5; 28 | } 29 | } 30 | 31 | error- { 32 | font-size: .5rem; 33 | color: #d50000; 34 | line-height: 138%; 35 | } 36 | 37 | label { 38 | width: 100%; 39 | } 40 | 41 | input { 42 | width: 100%; 43 | padding: .5rem; 44 | padding-left: 0; 45 | font-size: 1rem; 46 | border-bottom: 2px solid rgba(0,0,0,.12); 47 | background: #fff; 48 | border-radius: 0; 49 | &:focus { 50 | border-bottom: 2px solid #4285f4; 51 | } 52 | &[readonly] { 53 | opacity: 0.5; 54 | } 55 | } 56 | 57 | button[type='submit'] { 58 | padding: .5rem; 59 | width: auto; 60 | font-size: .62rem; 61 | text-transform: uppercase; 62 | border: 1px solid rgba(0,0,0,.1); 63 | cursor: pointer; 64 | background-color: #1e88e5; 65 | color: #fff; 66 | transition: box-shadow .2s; 67 | border-radius: 2px; 68 | box-shadow: 0 1px 2px 0 rgba(0,0,0,.39); 69 | &:hover, 70 | &:focus { 71 | box-shadow: 0 2px 5px 0 rgba(0,0,0,.39); 72 | } 73 | } 74 | } 75 | --------------------------------------------------------------------------------