├── .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 |
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 | 
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 |
126 | {/if}
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------