├── .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 | 
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 |
--------------------------------------------------------------------------------