Skip to content

Commit 99b24fe

Browse files
committed
initial commit - application and README
0 parents  commit 99b24fe

17 files changed

Lines changed: 809 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
public/bundle.js

README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Spotify Authorization With React + React-Router
2+
3+
This is an example application demonstrating authenticating a user
4+
[against the Spotify Web API][sag], using [React][r] and [React-Router][rr]
5+
and [Redux][rx] and [React-Router-Redux][rrr].
6+
7+
## Similarities to Spotify's [Web Auth Examples][wae]
8+
9+
This example is a variation on the `authorization_code` demo from Spotify's
10+
[Web Auth Examples][wae]. The main difference is the client code; whereas their
11+
example is contained in one `index.html` file, this example shows how to do the
12+
same thing with React and React-Router.
13+
14+
The other difference is the updated server code. Instead of using `request`
15+
directly (and XHR in the browser), this example interfaces with Spotify through
16+
the [Spotify Web API Node Module][swn] (and [Spotify Web Api Client][swj] in the
17+
browser). It also uses fun ES6 goodness. I opened a [pull request][spr] with
18+
them to update their server code to what you see here.
19+
20+
## Client Code Structure
21+
22+
The client code is built with [React][r] and [React-Router][rr] and [Redux][rx]
23+
and [React-Router-Redux][rrr]. phew!
24+
25+
The only real config this requires is in `client/index.js`:
26+
27+
~~~js
28+
class Root extends Component {
29+
render() {
30+
return (
31+
<Provider store={store}>
32+
<Router history={hashHistory}>
33+
<Route path="/" component={App}>
34+
<IndexRoute component={Login} />
35+
<Route path="/user/:accessToken/:refreshToken" component={User} />
36+
<Route path="/error/:errorMsg" component={Error} />
37+
</Route>
38+
</Router>
39+
</Provider>
40+
);
41+
}
42+
}
43+
~~~
44+
45+
Here, we initialize redux with our store, initialize react router with its
46+
history object. Everything else is a fairly traditional React app - the
47+
components are in `client/components`, the actions are in `client/actions`,
48+
and the reducer is in `client/reducers`.
49+
50+
## Server Code Structure
51+
52+
Under the `server` directory are two files `app.js` and `routes.js`. `app.js`
53+
handles all the setup, and all the routes are in, well, `routes.js`.
54+
55+
## Application Flow
56+
57+
The basic flow is this: client hits `/login`, gets redirected to Spotify's auth
58+
url, then gets redirected to `/callback`. If all is good and dandy, we send the
59+
client to `/#/user/${access_token}/${refresh_token}` which triggers the User
60+
page to load via React-Router. If all ain't good, we redirect the client to
61+
`/#/error/${error message}` which triggers the Error page to load via
62+
React-Router.
63+
64+
Once the client has the tokens, it requests information from spotify directly
65+
through use of the [Spotify Web API Client][swj]. This happens in
66+
`client/actions`, and the resulting data is interpreted through our reducer.
67+
Once the client has the data, `User.js` defines how it renders.
68+
69+
## Set Up
70+
71+
Make sure you create your application, get your id and secret, and register
72+
your callback url - `localhost:3000/callback` is what I used - by following
73+
[Spotify's Getting Started Guide][sgs].
74+
75+
## Running
76+
77+
The first thing you'll need to do is set your applications client id, client
78+
secret, and callback url. You can do this via the environment variables
79+
`client_id`, `client_secret`, and `redirect_uri`. Or by typing them into the
80+
code in `server/routes.js`. Fun tip: because we're using [Better NPM Run][bnr],
81+
you can set these in your `package.json` - head over there to see an example.
82+
83+
There are three scripts - `start`, `dev`, and `build`.
84+
85+
To run the production bundle:
86+
87+
~~~bash
88+
$ npm run build
89+
$ npm start
90+
~~~
91+
92+
To run in dev mode (with hot reloading, and un-minified source maps):
93+
94+
~~~bash
95+
$ npm run dev
96+
~~~
97+
98+
## Further Reading
99+
100+
The application structure is a simplified version of my
101+
[React + Redux + Webpack Boilerplate][bp] for better ease of understanding.
102+
It can certainly be awesome-ified (and maybe a little more complicated) by
103+
doing some of the fun tricks in there.
104+
105+
- [Spotify's Getting Started Guide][sgs]
106+
- [Spotify's Web API Authorization Guide][sag]
107+
- [Spotify Web API Node][swn]
108+
- [Spotify Web API JS/Client][swj]
109+
- [Spotify's Web API Auth Exampls][wae]
110+
- [My Pull Request enhancing Spotify's examples][spr]
111+
- [React Router][rr]
112+
- [React Router Redux][rrr]
113+
- [React][r]
114+
- [Redux][rx]
115+
- [Better NPM Run][bnr]
116+
- [React + Redux + Webpack Boilerplate][bp]
117+
118+
[sgs]: https://developer.spotify.com/web-api/tutorial/
119+
[sag]: https://developer.spotify.com/web-api/authorization-guide/
120+
[swn]: https://github.com/JMPerez/spotify-web-api-node
121+
[swj]: https://github.com/JMPerez/spotify-web-api-js
122+
[wae]: https://github.com/spotify/web-api-auth-examples
123+
[spr]: https://github.com/spotify/web-api-auth-examples/pull/7
124+
[rr]: https://github.com/rackt/react-router
125+
[rrr]: https://github.com/rackt/react-router-redux
126+
[r]: https://facebook.github.io/react/
127+
[rx]: http://redux.js.org/
128+
[bnr]: https://www.npmjs.com/package/better-npm-run
129+
[bp]: https://github.com/kauffecup/react-redux-webpack-boilerplate

client/actions/actions.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Spotify from 'spotify-web-api-js';
2+
const spotifyApi = new Spotify();
3+
4+
// our constants
5+
export const SPOTIFY_TOKENS = 'SPOTIFY_TOKENS';
6+
export const SPOTIFY_ME_BEGIN = 'SPOTIFY_ME_BEGIN';
7+
export const SPOTIFY_ME_SUCCESS = 'SPOTIFY_ME_SUCCESS';
8+
export const SPOTIFY_ME_FAILURE = 'SPOTIFY_ME_FAILURE';
9+
10+
/** set the app's access and refresh tokens */
11+
export function setTokens({accessToken, refreshToken}) {
12+
if (accessToken) {
13+
spotifyApi.setAccessToken(accessToken);
14+
}
15+
return { type: SPOTIFY_TOKENS, accessToken, refreshToken };
16+
}
17+
18+
/* get the user's info from the /me api */
19+
export function getMyInfo() {
20+
return dispatch => {
21+
dispatch({ type: SPOTIFY_ME_BEGIN});
22+
spotifyApi.getMe().then(data => {
23+
dispatch({ type: SPOTIFY_ME_SUCCESS, data: data });
24+
}).catch(e => {
25+
dispatch({ type: SPOTIFY_ME_FAILURE, error: e });
26+
});
27+
};
28+
}

client/components/App.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { Component } from 'react';
2+
3+
/**
4+
* Main app component
5+
* Has a header and then render's the page content
6+
*/
7+
export default class SpotifyLogin extends Component {
8+
render() {
9+
// injected via react router
10+
const {children} = this.props;
11+
return (
12+
<div className="spotify-login">
13+
<h1>Example Spotify + React + React-Router Login Flow</h1>
14+
<div className="page-content">
15+
<p>This is an example of the Authorization Code flow using routes.</p>
16+
{children}
17+
</div>
18+
</div>
19+
);
20+
}
21+
}

client/components/Error.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, { Component } from 'react';
2+
3+
/**
4+
* Our error page
5+
* Displays the error
6+
*/
7+
export default class Login extends Component {
8+
render() {
9+
// injected via react-router
10+
const { errorMsg } = this.props.params;
11+
return (
12+
<div className="error">
13+
<h2>An Error Occured</h2>
14+
<p>{errorMsg}</p>
15+
</div>
16+
);
17+
}
18+
}

client/components/Login.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Component } from 'react';
2+
import loginSVG from '../log_in.svg';
3+
4+
/**
5+
* Our login page
6+
* Has a login button that hit's the login url
7+
*/
8+
export default class Login extends Component {
9+
render() {
10+
return (
11+
<div className="login">
12+
<h2>Here's our login page!</h2>
13+
<a href="/login" dangerouslySetInnerHTML={{__html: loginSVG}}></a>
14+
</div>
15+
);
16+
}
17+
}

client/components/User.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { Component } from 'react';
2+
import { connect } from 'react-redux';
3+
import {
4+
getMyInfo,
5+
setTokens,
6+
} from '../actions/actions';
7+
8+
/**
9+
* Our user page
10+
* Displays the user's information
11+
*/
12+
class User extends Component {
13+
/** When we mount, get the tokens from react-router and initiate loading the info */
14+
componentDidMount() {
15+
// params injected via react-router, dispatch injected via connect
16+
const {dispatch, params} = this.props;
17+
const {accessToken, refreshToken} = params;
18+
dispatch(setTokens({accessToken, refreshToken}));
19+
dispatch(getMyInfo());
20+
}
21+
22+
/** Render the user's info */
23+
render() {
24+
const { accessToken, refreshToken, user } = this.props;
25+
const { loading, display_name, images, id, email, external_urls, href, country, product } = user;
26+
const imageUrl = images[0] ? images[0].url : "";
27+
// if we're still loading, indicate such
28+
if (loading) {
29+
return <h2>Loading...</h2>;
30+
}
31+
return (
32+
<div className="user">
33+
<h2>{`Logged in as ${display_name}`}</h2>
34+
<div className="user-content">
35+
<img src={imageUrl} />
36+
<ul>
37+
<li><span>Display name</span><span>{display_name}</span></li>
38+
<li><span>Id</span><span>{id}</span></li>
39+
<li><span>Email</span><span>{email}</span></li>
40+
<li><span>Spotify URI</span><span><a href={external_urls.spotify}>{external_urls.spotify}</a></span></li>
41+
<li><span>Link</span><span><a href={href}>{href}</a></span></li>
42+
<li><span>Profile Image</span><span><a href={imageUrl}>{imageUrl}</a></span></li>
43+
<li><span>Country</span><span>{country}</span></li>
44+
<li><span>Product</span><span>{product}</span></li>
45+
</ul>
46+
</div>
47+
</div>
48+
);
49+
}
50+
}
51+
52+
export default connect(state => state)(User);

client/index.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { Component } from 'react';
2+
import { render } from 'react-dom';
3+
import { createStore, combineReducers, applyMiddleware } from 'redux';
4+
import thunk from 'redux-thunk';
5+
import { Provider } from 'react-redux';
6+
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
7+
import { syncHistory, routeReducer } from 'react-router-redux';
8+
import { createHistory } from 'history';
9+
import reducer from './reducers';
10+
import App from './components/App';
11+
import Login from './components/Login';
12+
import User from './components/User';
13+
import Error from './components/Error';
14+
15+
// load our css. there probably is a better way to do this
16+
// but for now this is our move
17+
require('./style.less');
18+
19+
// Sync dispatched route actions to the history
20+
const reduxRouterMiddleware = syncHistory(hashHistory)
21+
const createStoreWithMiddleware = applyMiddleware(
22+
thunk,
23+
reduxRouterMiddleware
24+
)(createStore)
25+
const store = createStoreWithMiddleware(reducer)
26+
27+
class Root extends Component {
28+
render() {
29+
return (
30+
<Provider store={store}>
31+
<Router history={hashHistory}>
32+
<Route path="/" component={App}>
33+
<IndexRoute component={Login} />
34+
<Route path="/user/:accessToken/:refreshToken" component={User} />
35+
<Route path="/error/:errorMsg" component={Error} />
36+
</Route>
37+
</Router>
38+
</Provider>
39+
);
40+
}
41+
}
42+
43+
// render town
44+
const rootElement = document.getElementById('root');
45+
render(<Root />, rootElement);

client/log_in.svg

Lines changed: 51 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)