ReactJs & MSAL.js Unauthorized 401 - javascript

I have a pretty basic React App which the user can login and hit some MS Graph endpoint(s), this works great no problem once consent has been approved by the user. I also have another request firing to an anonymous Azure Function that spits back some JSON, brilliant no problem.
The issue I'm having is related to when I'm trying to make a request to an Azure Function Api that is locked down using AD with the MSAL.js lib.
I'm using https://github.com/sunilbandla/react-msal-sample.
App.js
import React, { Component } from 'react';
import './App.css';
import AuthService from './services/auth.service';
import GraphService from './services/graph.service';
import HelloService from './services/hello.service';
class App extends Component {
constructor() {
super();
this.authService = new AuthService();
this.graphService = new GraphService();
this.helloService = new HelloService();
this.state = {
user: null,
userInfo: null,
apiCallFailed: false,
loginFailed: false
};
}
componentWillMount() {}
callAPI = () => {
this.setState({
apiCallFailed: false
});
this.authService.getToken().then(
token => {
this.graphService.getUserInfo(token).then(
data => {
this.setState({
userInfo: data
});
},
error => {
console.error(error);
this.setState({
apiCallFailed: true
});
}
);
},
error => {
console.error(error);
this.setState({
apiCallFailed: true
});
}
);
};
callHelloAPI = () => {
this.setState({
apiCallFailed: false
});
this.authService.getToken().then(
token => {
this.helloService.callApi(token).then(
data => {
this.setState({
userInfo: data
});
console.log(data);
},
error => {
console.error(error);
this.setState({
apiCallFailed: true
});
}
);
},
error => {
console.error(error);
this.setState({
apiCallFailed: true
});
}
);
};
getTheToken = () => {
console.log('Get Token:');
this.authService.getToken().then(token => {
console.log(token);
});
}
logout = () => {
this.authService.logout();
};
login = () => {
this.setState({
loginFailed: false
});
this.authService.login().then(
user => {
if (user) {
console.log(user);
this.setState({
user: user
});
} else {
this.setState({
loginFailed: true
});
}
},
() => {
this.setState({
loginFailed: true
});
}
);
};
render() {
let templates = [];
if (this.state.user) {
templates.push(
<div key="loggedIn">
<button onClick={this.callAPI} type="button">
Call MS Graph
</button>
<button onClick={this.callHelloAPI} type="button">
Call Azure Function
</button>
<button onClick={this.getTheToken}>
Get The Token (JWT / Cookie)
</button>
<button onClick={this.logout} type="button">
Logout
</button>
<h3>Hello {this.state.user.name}</h3>
<h4>{this.state.user.displayableId}</h4>
</div>
);
} else {
templates.push(
<div key="loggedIn">
<button onClick={this.login} type="button">
Login with Microsoft
</button>
</div>
);
}
if (this.state.userInfo) {
templates.push(
<pre key="userInfo">{JSON.stringify(this.state.userInfo, null, 4)}</pre>
);
}
if (this.state.loginFailed) {
templates.push(<strong key="loginFailed">Login unsuccessful</strong>);
}
if (this.state.apiCallFailed) {
templates.push(
<strong key="apiCallFailed">Graph API call unsuccessful</strong>
);
}
return (
<div className="App">
<header className="App-header">
<h1 className="App-title">React app with MSAL.js</h1>
</header>
{templates}
</div>
);
}
}
export default App;
auth.service
import * as Msal from 'msal';
export default class AuthService {
constructor() {
let PROD_REDIRECT_URI = 'http://localhost:3000/';
let redirectUri = window.location.origin;
if (window.location.hostname !== '127.0.0.1') {
redirectUri = PROD_REDIRECT_URI;
}
this.applicationConfig = {
clientID: 'xxxx-xxxxx-xxxxx-xxxx',
graphScopes: ['user.read','user.readbasic.all']
};
this.app = new Msal.UserAgentApplication(
this.applicationConfig.clientID,
'',
() => {
// callback for login redirect
},
{
redirectUri
}
);
}
login = () => {
return this.app.loginPopup(this.applicationConfig.graphScopes).then(
idToken => {
const user = this.app.getUser();
if (user) {
return user;
} else {
return null;
}
},
() => {
return null;
}
);
};
logout = () => {
this.app.logout();
};
getToken = () => {
return this.app.acquireTokenSilent(this.applicationConfig.graphScopes).then(
accessToken => {
return accessToken;
},
error => {
return this.app
.acquireTokenPopup(this.applicationConfig.graphScopes)
.then(
accessToken => {
return accessToken;
},
err => {
console.error(err);
}
);
}
);
};
}
graph.service
export default class GraphService {
constructor() {
this.graphUrl = 'https://graph.microsoft.com/v1.0/users';
}
getUserInfo = token => {
const headers = new Headers({ Authorization: `Bearer ${token}` });
const options = {
headers
};
return fetch(this.graphUrl, options)
.then(response => response.json())
.catch(response => {
throw new Error(response);
});
};
}
hello.service
export default class HelloService {
constructor() {
this.graphUrl = 'https://xxx.azurewebsites.net/api/Hey';
}
callApi = token => {
const headers = new Headers({ Authorization: `Bearer ${token}` });
const options = {
headers
};
return fetch(this.graphUrl, options)
.then(response => response.json())
.catch(response => {
throw new Error(response);
});
};
}
Calling this hello.service works fine anonymously but returns a 401 when locked down by an Azure AD App. I've verified the JWT token from jwt.io also. I'm guessing the problem is an auth issue (hopefully a simple one), would an issue arise depending if the app is an Azure AD only app or a converged app? I have also enabled CORS on the function app itself.
My brain has frozen at this point & I'm not sure which direction facing. Any help would be greatly appreciated thanks in advance.

Related

Component doesn't re-render on follow up request after token refresh

This is my axios-hoook.js and I am using axios-hooks package.
import useAxios from 'axios-hooks';
import axios from 'axios';
import LocalStorageService from './services/local-storage.service';
import refreshToken from './refresh-token';
axios.defaults.baseURL = 'http://localhost:3000/api/v1';
axios.defaults.transformResponse = [
(responseData) => {
const { data, error } = JSON.parse(responseData);
return error || data;
},
];
// request interceptor to add token to request headers
axios.interceptors.request.use(
async (config) => {
const token = LocalStorageService.getAccessToken();
if (token) {
config.headers = {
authorization: token,
};
}
return config;
},
(error) => Promise.reject(error)
);
// response interceptor intercepting 401 responses, refreshing token and retrying the request
axios.interceptors.response.use(
(response) => response,
(error) => {
const { config } = error;
if (error.response?.status === 401 && !config._retry) {
config._retry = true;
refreshToken(LocalStorageService.getRefreshToken())
.then((res) => {
const { accessToken } = res.data.data;
LocalStorageService.setAccessToken(accessToken);
return axios(config);
})
.catch((err) => {
if (err.response.status === 401) {
LocalStorageService.setUser(null);
window.location.href = '/login';
}
return Promise.reject(err);
});
}
return Promise.reject(error);
}
);
export default useAxios;
This is the Course.jsx where it is being used.
const Course = () => {
const [{ data: courses = [] }, refetchCourse] = axiosHook(ApiConfig.COURSE.GET_COURSES.url);
return (
<Datatable
entity={entity}
columns={courseColumns}
rows={courses}
deleteRow={handleDeactivate}
viewRow={handleView}
/>
)
}
Image
In the axios-hooks docs there's a link to a working example to implement a refresh token feature. It is running in CodeSandbox at this link https://codesandbox.io/s/axios-hooks-authentication-zyeyh.
Compare that with your example and you'll find the reason why yours doesn't work.
There was an error in my code. I was not returning the promise.
axios.interceptors.response.use(
(response) => response,
(error) => {
const { config } = error;
if (error.response?.status === 401 && !config._retry) {
config._retry = true;
return refreshToken(LocalStorageService.getRefreshToken()) // this line
.then((res) => {
const { accessToken } = res.data.data;
LocalStorageService.setAccessToken(accessToken);
return axios(config);
})
.catch((err) => {
if (err.response.status === 401) {
LocalStorageService.setUser(null);
window.location.href = '/login';
}
return Promise.reject(err);
});
}
return Promise.reject(error);
}
);

Nuxt middleware to check if user is logged in not working

I am trying to check if a user is authenticated and redirect them depending on the page they are in. for example if the user is logged in and they try to visit the login or signup page, they should be redirected. I have a middleware for that.
when I log in the user, the authenticateUser action runs and the user is created, when I check my cookies and local storage on the browser, I see that it is set correctly, but when I visit the login page after logging in, it doesn't redirect me.
middleware/altauth.js
export default function (context) {
console.log(context.store.getters('profile/isAuthenticated'))
if (context.store.getters.isAuthenticated) {
context.redirect('/')
}
}
also the token is both saved using Cookies and local storage and is persistence is through this middleware
middleware/checkauth.js
export default function (context) {
if(context.hasOwnProperty('ssrContext')) {
context.store.dispatch('profile/initAuth', context.ssrContext.req);
} else {
context.store.dispatch('profile/initAuth', null);
}
}
and below are the values for my store
import Cookie from 'js-cookie';
export const state = () => ({
token: null,
})
export const mutations = {
setToken(state, token) {
state.token = token
},
clearToken(state) {
state.token = null
}
}
export const actions = {
async authenticateUser(vuexContext, authData) {
let authUrl = 'https://look.herokuapp.com/signup/'
if (authData.isLogin) {
authUrl = 'https://look.herokuapp.com/login/'
}
return this.$axios
.$post(authUrl, authData.form)
.then(data => {
console.log(data);
const token = data.token
vuexContext.commit('setToken', token)
localStorage.setItem("token", token)
Cookie.set('jwt', token);
})
.catch(e => console.log(e))
},
initAuth(vuexContext, req) {
let token
if (req) {
if (!req.headers.cookie) {
return;
}
const jwtCookie = req.headers.cookie
.split(';')
.find(c => c.trim().startsWith('jwt='));
if (!jwtCookie) {
return;
}
token = jwtCookie.split('=')[1];
} else {
token = localStorage.getItem('token');
if (!token) {
return;
}
}
vuexContext.commit('setToken', token);
}
}
export const getters = {
isAuthenticated(state) {
return state.token != null;
},
}
please help, i don't know what the problem can be
Here is a basic but full example for auth system in SSR nuxt
You will need two apis for this, one will return token info with user info, and the other will return user info only.
for example
POST http://example.com/api/auth/authorizations
{
token: 'abcdefghijklmn',
expired_at: 12345678,
user: {
name: 'Tom',
is_admin: true
}
}
// this need authed
GET http://example.com/api/auth/user
{
name: 'Tom',
is_admin: true
}
nuxt.config.js
plugins:[
'~plugins/axios',
],
buildModules: [
'#nuxtjs/axios',
],
router: {
middleware: [
'check-auth'
]
},
./pages/login.vue
<template>
<form #submit.prevent="login">
<input type="text" name="username" v-model="form.username">
<input type="password" name="password" v-model="form.password">
</form>
</template>
<script type="text/javascript">
export default{
data(){
return {
form: {username: '', password: ''}
}
},
methods: {
login(){
this.$axios.post(`/auth/authorizations`, this.form)
.then(({ data }) => {
let { user, token } = data;
this.$store.commit('auth/setToken', token);
this.$store.commit('auth/updateUser', user);
this.$router.push('/');
})
}
}
}
</script>
store/index.js
const cookieFromRequest = (request, key) => {
if (!request.headers.cookie) {
return;
}
const cookie = request.headers.cookie.split(';').find(
c => c.trim().startsWith(`${key}=`)
);
if (cookie) {
return cookie.split('=')[1];
}
}
export const actions = {
nuxtServerInit({ commit, dispatch, route }, { req }){
const token = cookieFromRequest(req, 'token');
if (!!token) {
commit('auth/setToken', token);
}
}
};
middleware/check-auth.js
export default async ({ $axios, store }) => {
const token = store.getters['auth/token'];
if (process.server) {
if (token) {
$axios.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete $axios.defaults.headers.common.Authorization;
}
}
if (!store.getters['auth/check'] && token) {
await store.dispatch('auth/fetchUser');
}
}
store/auth.js
import Cookie from 'js-cookie';
export const state = () => ({
user: null,
token: null
});
export const getters = {
user: state => state.user,
token: state => state.token,
check: state => state.user !== null
};
export const mutations = {
setToken(state, token){
state.token = token;
},
fetchUserSuccess(state, user){
state.user = user;
},
fetchUserFailure(state){
state.user = null;
},
logout(state){
state.token = null;
state.user = null;
},
updateUser(state, { user }){
state.user = user;
}
}
export const actions = {
saveToken({ commit }, { token, remember }){
commit('setToken', token);
Cookie.set('token', token);
},
async fetchUser({ commit }){
try{
const { data } = await this.$axios.get('/auth/user');
commit('fetchUserSuccess', data);
}catch(e){
Cookie.remove('token');
commit('fetchUserFailure');
}
},
updateUser({ commit }, payload){
commit('updateUser', payload);
},
async logout({ commit }){
try{
await this.$axios.delete('/auth/authorizations');
}catch(e){}
Cookie.remove('token');
commit('logout');
}
}
plugins/axios.js
export default ({ $axios, store }) => {
$axios.setBaseURL('http://example.com/api');
const token = store.getters['auth/token'];
if (token) {
$axios.setToken(token, 'Bearer')
}
$axios.onResponseError(error => {
const { status } = error.response || {};
if (status === 401 && store.getters['auth/check']) {
store.commit('auth/logout');
}
else{
return Promise.reject(error);
}
});
}
Then you can do what you want in your middleware, such as check auth
middleware/auth.js
export default function ({ store, redirect }){
if (!store.getters['auth/check']) {
return redirect(`/login`);
}
}

Spring RSocket Security + RSocket-WebSocket-Client (browser)

I am trying to make a site in Vue and backend on Spring. I want to use rsocket to transfer data, but as soon as I add rsocket seurity in spring, I get :
'metadata is malformed'
Would like to take a look at a working example using jwt/simpleauth
I solved the issue with Simple Auth, now I would like to synchronize this authorization with spring websecurity.
Those. so that routing in rsocket checks authorization via websecurity. I know that this can be implemented through the jwt token, i.e. send a jwt token to a client via rest, but how can I do this in code? JS client (browser) and Spring, how do I generate userdetails token?
Just in case, I'll leave an example of the simpleauth implementation:
// METADATA BUILDER
import {encodeRoute, encodeBearerAuthMetadata, encodeSimpleAuthMetadata, encodeAndAddCustomMetadata, encodeAndAddWellKnownMetadata, MESSAGE_RSOCKET_ROUTING, MESSAGE_RSOCKET_AUTHENTICATION} from "rsocket-core";
export default class Metadata {
constructor(json) {
this.route = json['route'];
this.auth = json['auth'];
}
toMetadata() {
let metadata = Buffer.alloc(0);
if (this.auth) {
if (this.auth["type"] === 'bearer') {
metadata = encodeAndAddCustomMetadata(
metadata,
MESSAGE_RSOCKET_AUTHENTICATION.string,
encodeBearerAuthMetadata(this.auth["token"]),
);
}
if (this.auth["type"] === 'simple') {
metadata = encodeAndAddCustomMetadata(
metadata,
MESSAGE_RSOCKET_AUTHENTICATION.string,
encodeSimpleAuthMetadata(this.auth["username"], this.auth["password"]),
);
}
}
if (this.route) {
metadata = encodeAndAddWellKnownMetadata(
metadata,
MESSAGE_RSOCKET_ROUTING,
encodeRoute(this.route)
);
}
return metadata;
}
}
// RSOCKET CLIENT CLASS
import RSocketWebSocketClient from "rsocket-websocket-client";
import {BufferEncoders, MESSAGE_RSOCKET_COMPOSITE_METADATA, RSocketClient,toBuffer} from "rsocket-core";
import Metadata from "./metadata";
export default class SpringClient {
constructor(wsUrl, keepAlive = 60000, lifetime = 180000, dataMimeType = "application/json") {
this.client = new RSocketClient({
"setup": {
"keepAlive": keepAlive,
"lifetime": lifetime,
"dataMimeType": dataMimeType,
"metadataMimeType": MESSAGE_RSOCKET_COMPOSITE_METADATA.string
},
"transport": new RSocketWebSocketClient({
"url": wsUrl
}, BufferEncoders)
});
}
bearerAuth(token) {
this.auth = {type: "bearer", token: token}
}
simpleAuth(username, password) {
this.auth = {type: "simple", username: username, password: password}
}
logout() {
this.auth = null;
}
connect(
completeCallback = (socket) => {
}, errorCallback = (error) => {
}, subscribeCallback = (cancel) => {
}
) {
this.client.connect().subscribe({
onComplete: socket => {
this.socket = socket;
completeCallback(socket);
},
onError: error => {
errorCallback(error);
},
onSubscribe: cancel => {
subscribeCallback(cancel);
}
});
}
requestResponse(data, route,
completeCallback = (data) => {
},
errorCallback = (error) => {
},
subscribeCallback = (cancel) => {
}
) {
if (this.socket) {
const metadata = new Metadata({
route: route,
auth: this.auth
}).toMetadata();
data = toBuffer(data);
this.socket.requestResponse({
data,
metadata
}).subscribe({
onComplete: data => {
completeCallback(data);
},
onError: error => {
errorCallback(error);
},
onSubscribe: cancel => {
subscribeCallback(cancel);
}
});
}
}
}
// EXAMPLE, HOW TO USE
import SpringClient from "./springclient";
this.client = new SpringClient("ws://localhost:7000/", 5000, 15000, "text/plain");
this.client.connect(
(socket) => {
console.log("got connection complete");
this.socket = socket;
},
(error) => {
console.log("got connection error");
console.error(error);
},
(cancel) => {
console.log("got connection subscribe");
/* call cancel() to abort */
}
)
this.client.simpleAuth("LOGIN", "PASSWORD");
this.client.requestResponse("MESSAGE", "ROUTE",
(data) => {
console.log("got response with requestResponse");
console.log(data.data);
},
(error) => {
console.log("got error with requestResponse");
console.error(error);
},
(cancel) => {
console.log(message);
/* call cancel() to stop onComplete/onError */
}
);

Can I find out the real cause of this error? (React Native)

First I apologize for my English. I have been working on React to native applications for 4 months. But sometimes I get this error and don't mind.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, the componentWillUnmount method,
in CustomerDetailScreen (at SceneView.tsx:123)
This is because when I click the button, I open a screen, and when the screen is not fully loaded I press the back button. How can I resolve this warning? I'll show you some sample code examples.
I hope I could explain. Can you help me with this topic? I want to fully understand the logic. I've read something called AbortController in some articles but I don't know exactly.
constructor(props) {
super(props);
this._isMounted = false;
this.state = {
id: props.route.params.id,
username: '',
token: null,
cityId: 1,
townId: 1,
cityData: [],
townData: [],
selectedIndex: 0,
selectedCity: new IndexPath(0),
selectedTown: new IndexPath(0),
}
}
componentDidMount() {
this._isMounted = true;
this._isMounted && this._getToken();
}
componentWillUnmount() {
this._isMounted = false;
}
_getToken = async () => {
try {
const username = await AsyncStorage.getItem('username');
const token = await AsyncStorage.getItem('token');
if(token === null) {
await AsyncStorage.removeItem('token');
}else {
this.setState({ username: username, token: token });
this._isMounted && this.loadCustomerDetail();
}
} catch (error) {
console.log(error);
}
};
loadCustomerDetail() {
try {
const { username, token } = this.state;
if(token) {
const { id } = this.state;
var credentials = Base64.btoa(username + ':' + token);
var URL = `https://portal.xxxxxx.com/api/v1/Customer/${id}`;
axios.get(URL, {headers : { 'Espo-Authorization' : credentials }})
.then(this.dataSuccess.bind(this))
.catch(this.dataFail.bind(this));
}
}catch (err) {
console.log(err);
}
};
dataSuccess(response) {
this.setState({
isLoading: false,
cityId: response.data.cityId,
townId: response.data.townId
}, () => {
this.getCities();
});
}
getCities() {
const { username, token, cityId } = this.state;
let credentials = Base64.btoa(username + ':' + token);
axios.get('https://portal.xxxxx.com/api/v1/Cities', { headers : { 'Espo-Authorization' : credentials }})
.then((response) => {
response.data.list.sort(function(a, b) {
return Number(a.id) > Number(b.id);
});
this.setState({cityData: response.data.list}, () => {
this.setState({ selectedCity: new IndexPath(this.state.cityData[cityId-1].id - 1) });
this.getTowns(this.state.cityData[cityId-1].id);
});
}).catch((error) => {
console.log(error);
});
}
getTowns(cityId) {
this.setState({ townLoading: true });
const { username, token } = this.state;
let credentials = Base64.btoa(username + ':' + token);
axios.get(`https://portal.xxxxx.com/api/v1/Towns/action/TownList?cityId=${cityId}`, { headers : { 'Espo-Authorization' : credentials }})
.then((response) => {
this.setState({ townData: response.data, townLoading: false }, () => {
for (const [key, value] of Object.entries(this.state.townData)) {
if(value.id === this.state.townId) {
this.setState({ selectedTown: new IndexPath(key) })
}
}
});
}).catch((error) => {
console.log(error);
});
}
An example area:
this.setState({ username: username, token: token });
this._isMounted && this.loadCustomerDetail();
You can see that setState will be called even if the component is no longer mounted.
Fix
Ensure component is mounted before changing state:
if (this._isMounted) {
this.setState({ username: username, token: token });
this.loadCustomerDetail();
}

TypeError: Cannot read property 'then' of undefined, React app error when trying to save a playlist

Can't seem to resolve this error.
TypeError
For the function to work the user is supposed to be able to add songs to a 'New Playlist' and then save that playlist.
The app seems to work fine until you click the button to save the playlist at which point all you see is this error.
Trying to access this component:
const clientId = "783dbc97776940e28f307dfc902ad41b";
const redirectUri = "http//localhost:3000/";
let accessToken;
const Spotify = {
getAccessToken() {
if (accessToken) {
return accessToken;
}
// check for access token match
const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
const expiresInMatch = window.location.href.match(/expires_in=([^&]*)/);
if (accessTokenMatch && expiresInMatch) {
accessToken = accessTokenMatch[1];
const expiresIn = Number(expiresInMatch[1]);
// This clears the parameters, allowing us to grab a new access token when it expires.
window.setTimeout(() => (accessToken = ""), expiresIn * 1000);
window.history.pushState("Access Token", null, "/");
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
search(term) {
const accessToken = Spotify.getAccessToken();
return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
})
.then(response => {
return response.json();
})
.then(jsonResponse => {
if (!jsonResponse.tracks) {
return [];
}
return jsonResponse.tracks.items.map(track => ({
id: track.id,
name: track.name,
artist: track.artists[0].name,
album: track.album.name,
uri: track.uri
}));
});
},
savePlaylist(name, trackUris) {
if (!name || !trackUris.length) {
return;
}
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}` };
let userId;
return fetch("https://api.spotify.com/v1/me", { headers: headers })
.then(response => response.json())
.then(jsonResponse => {
userId = jsonResponse.id;
return fetch(`https://api.spotify.com/v1/users/${userId}/playlists`, {
headers: headers,
method: "POST",
body: JSON.stringify({ name: name })
})
.then(response => response.json())
.then(jsonResponse => {
const playlistId = jsonResponse.id;
return fetch(
`https://api.spotify.com/v1/users/${userId}/playlists/${playlistId}/tracks`,
{
headers: headers,
method: "POST",
body: JSON.stringify({ uris: trackUris })
}
);
});
});
}
};
export default Spotify;
For use here:
import React from 'react';
import './App.css';
import SearchBar from '../SearchBar/SearchBar';
import SearchResults from '../SearchResults/SearchResults';
import Playlist from '../Playlist/Playlist';
import Spotify from '../../util/Spotify';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
playlistName: 'My Playlist',
playlistTracks: []
}
this.addTrack = this.addTrack.bind(this);
this.removeTrack = this.removeTrack.bind(this);
this.updatePlaylistName = this.updatePlaylistName.bind(this);
this.savePlaylist = this.savePlaylist.bind(this);
this.search = this.search.bind(this);
}
addTrack(track) {
if (this.state.playlistTracks.find(savedTrack => savedTrack.id === track.id)) {
return;
}
this.state.playlistTracks.push(track);
this.setState({playlistTracks: this.state.playlistTracks})
}
removeTrack(track) {
this.setState.playlistTracks = this.state.playlistTracks.filter(currentTrack => currentTrack.id !== track.id)
this.setState({playlistTracks: this.state.playlistTracks})
}
updatePlaylistName(name) {
this.setState({playlistName: name});
}
savePlaylist() {
const trackUris = this.state.playlistTracks.map(track => track.uri);
Spotify.savePlaylist(this.state.playlistName, trackUris).then(() => {
this.setState({
playlistName: 'New Playlist',
playlistTracks: []
})
})
}
search(term) {
Spotify.search(term).then(searchResults => {
this.setState({searchResults: searchResults})
})
}
render() {
return (
<div>
<h1>Ja<span className="highlight">mmm</span>ing</h1>
<div className="App">
<SearchBar onSearch={this.search} />
<div className="App-playlist">
<SearchResults searchResults={this.state.searchResults} onAdd={this.addTrack} />
<Playlist playlistName={this.state.playlistName} playlistTracks={this.state.playlistTracks} onRemove={this.removeTrack} onNameChange={this.updatePlaylistName} onSave={this.savePlaylist} />
</div>
</div>
</div>
)
}
}
// if(typeof(App.savePlaylist) == 'undefined') {
// console.log('whoops')
// }
export default App;
Any ideas why?
I'm very new to this and am completely lost as to why this is happening.
You need to return a Promise for when the input is not right:
if (!name || !trackUris.length) {
return Promise.resolve();
};
Also, don't change variables of the state object. Don't do this:
this.state.playlistTracks.push(track);
this.setState({playlistTracks: this.state.playlistTracks})
Instead, do this:
this.setState({playlistTracks: [...this.state.playlistTracks, track]})
In you code when the following if condition is true, you're not returning a promise.
savePlaylist(name, trackUris) {
if (!name || !trackUris.length) {
return;
// What you can do instead
// return Promise.reject();
};
....
...
Simply returning return; will return undefined and trying to access then on it will throw error.

Categories

Resources