├── .gitignore ├── .env.sample ├── src ├── index.css ├── index.js ├── auth │ ├── useMachine.js │ ├── index.js │ └── authMachine.js ├── Auth.svelte └── App.svelte ├── svelte.config.js ├── firebase-authentication-login-form.png ├── tailwind.config.js ├── postcss.config.js ├── public └── favicon.svg ├── prettier.config.js ├── index.html ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | VITE_FIREBASE_KEY=your-firebase-auth-key 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base.css'; 2 | @import 'tailwindcss/components.css'; 3 | @import 'tailwindcss/utilities.css'; 4 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | const { postcss } = require('svelte-preprocess'); 2 | 3 | module.exports = { 4 | preprocess: [postcss()] 5 | }; 6 | -------------------------------------------------------------------------------- /firebase-authentication-login-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codechips/svelte-firebase-auth-xstate-example/HEAD/firebase-authentication-login-form.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.svelte'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: {}, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | import './index.css'; 3 | 4 | const app = new App({ 5 | target: document.body, 6 | }); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('tailwindcss')(), 5 | require('postcss-preset-env')({ stage: 1 }) 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: true, 4 | singleQuote: true, 5 | printWidth: 80, 6 | plugins: ['prettier-plugin-svelte'], 7 | svelteSortOrder: 'styles-scripts-markup', 8 | svelteStrictMode: false, 9 | svelteBracketNewLine: true 10 | }; 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte App 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-firebase-auth-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "svite", 6 | "build": "svite build" 7 | }, 8 | "dependencies": { 9 | "svelte": "^3.24.0" 10 | }, 11 | "devDependencies": { 12 | "firebase": "^7.16.1", 13 | "postcss": "^7.0.32", 14 | "postcss-preset-env": "^6.7.0", 15 | "prettier": "^2.0.5", 16 | "prettier-plugin-svelte": "^1.1.0", 17 | "svelte-preprocess": "^4.0.8", 18 | "svite": "^0.3.0", 19 | "tailwindcss": "^1.5.2", 20 | "vite": "^1.0.0-rc.3", 21 | "xstate": "^4.11.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/auth/useMachine.js: -------------------------------------------------------------------------------- 1 | import { readable } from 'svelte/store'; 2 | import { interpret } from 'xstate'; 3 | 4 | export const useMachine = (machine, options) => { 5 | const service = interpret(machine, options); 6 | 7 | // wrap machine in a svelte readable store with 8 | // initial state (defined in config) as its starting state 9 | const store = readable(service.initialState, set => { 10 | // every time we change state onTransition 11 | // hook is triggered 12 | service.onTransition(state => { 13 | set(state); 14 | }); 15 | 16 | // start the machine service 17 | service.start(); 18 | 19 | return () => { 20 | service.stop(); 21 | }; 22 | }); 23 | 24 | // return a custom Svelte store 25 | return { 26 | state: store, 27 | send: service.send 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-firebase-auth-xstate-example 2 | 3 | > :lock: How to do Firebase authentication with XState and Svelte 4 | 5 | This is example code for my blog posts: 6 | 7 | - [Firebase authentication with Svelte](https://codechips.me/firebase-authentication-with-svelte/) 8 | - [Firebase authentication with Svelte and XState](https://codechips.me/firebase-authentication-with-xstate-and-svelte/). 9 | 10 | ![firebase auth login form](firebase-authentication-login-form.png) 11 | 12 | # Firebase config 13 | 14 | Make sure to put your Firebase API key in the `.env` file in the root directory. 15 | 16 | ```bash 17 | VITE_FIREBASE_KEY=your-firebase-auth-key 18 | ``` 19 | 20 | Also change your project name in `App.svelte` 21 | 22 | ## How to run 23 | 24 | Clone and run `npm i && npm run dev` 25 | 26 | To see the complete code for the first article do: 27 | 28 | ```text 29 | $ git checkout plain-firebase-auth 30 | ``` 31 | 32 | ## There is more! 33 | 34 | For more interesting stuff like this follow me on [Twitter](https://twitter.com/codechips) or check out my blog https://codechips.me 35 | 36 | -------------------------------------------------------------------------------- /src/Auth.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 | 58 |
59 | 60 |
61 | -------------------------------------------------------------------------------- /src/auth/index.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import { useMachine } from './useMachine'; 4 | import { initAuthMachine } from './authMachine'; 5 | 6 | const userMapper = claims => ({ 7 | id: claims.user_id, 8 | name: claims.name, 9 | email: claims.email, 10 | picture: claims.picture 11 | }); 12 | 13 | // construction function. need to call it after we 14 | // initialize our firebase app 15 | export const initAuth = (useRedirect = false) => { 16 | const auth = firebase.auth(); 17 | 18 | const loginWithEmailPassword = (email, password) => 19 | auth.signInWithEmailAndPassword(email, password); 20 | 21 | const loginWithGoogle = () => { 22 | const provider = new firebase.auth.GoogleAuthProvider(); 23 | 24 | return useRedirect 25 | ? auth.signInWithRedirect(provider) 26 | : auth.signInWithPopup(provider); 27 | }; 28 | 29 | const services = { 30 | authChecker: () => 31 | // wrap the onAuthStateChanged hook in a promise and 32 | // immediately unsubscribe when triggered 33 | new Promise((resolve, reject) => { 34 | const unsubscribe = firebase.auth().onAuthStateChanged(auth => { 35 | unsubscribe(); 36 | return auth ? resolve(auth) : reject(); 37 | }); 38 | }), 39 | authenticator: (_, event) => { 40 | if (event.provider === 'email') { 41 | return loginWithEmailPassword(event.email, event.password); 42 | } else if (event.provider === 'google') { 43 | return loginWithGoogle(); 44 | } 45 | }, 46 | loader: (ctx, _) => { 47 | return new Promise(resolve => { 48 | setTimeout(() => { 49 | // auth object is already set on the app context 50 | // by authChecker service 51 | ctx.auth 52 | .getIdTokenResult() 53 | .then(({ claims }) => userMapper(claims)) 54 | .then(resolve); 55 | }, 1500); 56 | }); 57 | }, 58 | logout: () => auth.signOut() 59 | }; 60 | 61 | const authMachine = initAuthMachine(services); 62 | 63 | return useMachine(authMachine); 64 | }; 65 | -------------------------------------------------------------------------------- /src/auth/authMachine.js: -------------------------------------------------------------------------------- 1 | import { createMachine, assign } from 'xstate'; 2 | 3 | const config = { 4 | id: 'auth', 5 | // we want to start by checking if 6 | // user is logged in when page loads 7 | initial: 'authenticating', 8 | // context is where you keep state 9 | context: { 10 | auth: null, 11 | user: null, 12 | error: null 13 | }, 14 | // all possible authentication states 15 | states: { 16 | authenticating: { 17 | // when entering a state invoke 18 | // the authChecker service 19 | invoke: { 20 | id: 'authChecker', 21 | src: 'authChecker', 22 | onDone: { target: 'loading', actions: 'setAuth' }, 23 | onError: { target: 'signedOut' } 24 | } 25 | }, 26 | // we will enrich the user profile 27 | // with additional data 28 | loading: { 29 | invoke: { 30 | id: 'loader', 31 | src: 'loader', 32 | onDone: { target: 'signedIn', actions: 'setUser' }, 33 | onError: { 34 | target: 'signedOut.failure', 35 | actions: ['setError', 'clearAuth'] 36 | } 37 | } 38 | }, 39 | signedIn: { 40 | // when receiving 'LOGOUT' event 41 | // transition to singingOut state 42 | on: { LOGOUT: { target: 'signingOut' } } 43 | }, 44 | // signedOut has two sub-states 45 | // we will transition to failure in 46 | // case of wrong password, username 47 | // or network error 48 | signedOut: { 49 | initial: 'ok', 50 | states: { 51 | ok: { type: 'final' }, 52 | failure: {} 53 | }, 54 | on: { 55 | LOGIN: { target: 'signingIn' } 56 | } 57 | }, 58 | signingIn: { 59 | invoke: { 60 | id: 'authenticator', 61 | src: 'authenticator', 62 | onDone: { 63 | target: 'authenticating', 64 | // clear error if successful login 65 | actions: 'clearError' 66 | }, 67 | onError: { 68 | // transition to failure state 69 | // and set an error 70 | target: 'signedOut.failure', 71 | actions: 'setError' 72 | } 73 | } 74 | }, 75 | signingOut: { 76 | invoke: { 77 | id: 'logout', 78 | src: 'logout', 79 | onDone: { 80 | target: 'signedOut', 81 | actions: ['clearAuth', 'clearError'] 82 | }, 83 | onError: { 84 | target: 'signedOut.failure', 85 | actions: ['clearAuth', 'setError'] 86 | } 87 | } 88 | } 89 | } 90 | }; 91 | 92 | export const initAuthMachine = services => { 93 | // define XState actions so that we can 94 | // refer to them by name in machine config 95 | const actions = { 96 | // clear user info on logout 97 | clearAuth: assign({ user: null, auth: null }), 98 | clearError: assign({ error: null }), 99 | // put Firebase auth object on context 100 | setAuth: assign({ auth: (_, event) => event.data }), 101 | // put user on context in loading service 102 | setUser: assign({ user: (_, event) => event.data }), 103 | setError: assign({ 104 | error: (_, event) => event.data 105 | }) 106 | }; 107 | 108 | // create an options object containing 109 | // actions and services 110 | const options = { 111 | actions, 112 | services 113 | }; 114 | 115 | return createMachine(config, options); 116 | }; 117 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 55 | 56 |
57 |
58 | {#if $state.matches('authenticating')} 59 |

Authenticating ...

60 | {/if} 61 | 62 | {#if $state.matches('loading')} 63 |

Loading ...

64 | {/if} 65 | 66 | 67 | 72 | 73 | {#if $state.matches('signedIn')} 74 |
75 |

{$state.context.user.email}

76 | 79 |
80 | {/if} 81 | 82 | {#if displayLoginForm} 83 |
87 |
88 | 89 | 95 |
96 |
97 | 98 | 104 |
105 | 106 | {#if $state.context.error} 107 |
108 | {$state.context.error} 109 |
110 | {/if} 111 |
112 | 113 |
114 |
115 | 116 | 124 |
125 |
126 | {/if} 127 | 128 |
129 |
130 | --------------------------------------------------------------------------------