Routing with Spotify API - javascript

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])

You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}

Related

MetaMask on page refresh with Web3.js and pure JS

I am trying to have the MetaMask wallet to stay connected upon the page refresh. However, I could not find any info on that matter on the world-wide-web. Sad. Also, from what I've managed to find, there was supposedly a MM privacy update in 2019, which cut out the injection method...so, is there a way to do it natively with pure JS?
The code so far:
const getWeb3 = async () => {
return new Promise(async (resolve, reject) => {
const web3 = new Web3(window.ethereum)
try {
await window.ethereum.request({ method: "eth_requestAccounts" })
resolve(web3)
} catch (error) {
reject(error)
}
})
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("connect_button").addEventListener("click", async ({ target }) => {
const web3 = await getWeb3()
const walletAddress = await web3.eth.requestAccounts()
const walletShort = walletAddress.toString()
const walletBalanceInWei = await web3.eth.getBalance(walletAddress[0])
const walletBalanceInEth = Math.round(Web3.utils.fromWei(walletBalanceInWei) * 100) / 100
target.setAttribute("hidden", "hidden")
document.getElementById("wallet_balance").innerText = walletBalanceInEth
document.getElementById("wallet_info").removeAttribute("hidden")
document.getElementById("address_shrt").innerText = walletShort.slice(0,3) + '...' + walletShort.slice(-3)
})
})
I have no idea about react whatsoever, so react guides are kinda gibberish to me. Any useful links or directions I could follow at least? Thanks in advance!
What you are trying to do is impossible by design. You have to re-request the address view permission every time your page refreshes (for privacy reasons).
u will stay connected on page refresh with these codes in react,next,next etc.
import { useState, useEffect } from "react";
export default function Home() {
const [ismetamask, setIsmetamask] = useState(false); // is metamask installed ?
const [accountaddress, setAccountaddress] = useState([]);
useEffect(() => {
if (window) {
//sometimes window is not loaded, so wait for windows loading
if (typeof window.ethereum !== "undefined") {
console.log("MetaMask is installed!");
setIsmetamask(true);
// check if metamask is already connected
if (window.ethereum._state.accounts.length > 0) {
// metamask is already connected
ethereum.request({ method: "eth_requestAccounts"});
setAccountaddress(window.ethereum._state.accounts[0]);
} else {
// metamask not connected yet
}
// trigger when account change: logout or login
ethereum.on("accountsChanged", function (accounts) {
if (window.ethereum._state.accounts.length > 0) {
setAccountaddress(window.ethereum._state.accounts[0]);
}else{
setAccountaddress([]);
}
});
} else {
console.log("metamask not installed");
}
} else {
console.log("window not loaded yet");
}
}, []);
const signinMetamask = async () => {
// const accounts =
await ethereum.request({ method: "eth_requestAccounts" });
// const account = accounts[0];
};
return (
<>
{ismetamask ? (
<>
{accountaddress.length < 1 ? (
<>
<button
onClick={() => {
signinMetamask();
}}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Connect Metamask
</button>
</>
) : (
<>user: {accountaddress}</>
)}
</>
) : (
<>metamask not installed</>
)}
</>
);
}

How to authenticate user with Context API?

I am having trouble logging in to my application using the Context API. When I run applications without having any token in my localStorage in the variable session I get a lot of errors like below:
Uncaught TypeError: Cannot read properties of null (reading 'name')
I think that this problem exists because my currentAccount from ApplicationContext is null.
dashboard/index.tsx
const { currentAccount } = useContext(ApplicationContext);
return (
<span>{currentAccount.name}</span>
);
On the routes.login login page I am also getting these exceptions even though this error should only be on routes.dashboard :/ Refreshing the page or clearing localStorage does not help. I;m having also an infinite loop over checkLogin in ApplicationContextProvider :(
login/index.tsx
const { setCurrentAccount } = useContext(ApplicationContext);
const onFinish = async (email: string; password: string) => {
try {
const response = await axios.post("/auth/login", {
email: email,
password: password
});
const token = response["token"];
const account = response["account"];
if (token && account) {
localStorage.setItem("session", token);
setCurrentAccount(account);
history.push(routes.dashboard)
}
} catch (error) {
}
};
App.tsx
return (
<div className="App">
<Switch>
<ApplicationContextProvider>
<Route path={route.login} component={Login} />
<Main>
<Route path={route.dashboard} component={Dashboard} />
</Main>
</ApplicationContextProvider>
</Switch>
</div>
);
ApplicationContextProvider.tsx
export type AccountContext = {
currentAccount?: Account;
setCurrentAccount: (user: Account) => void;
checkLogin: () => void;
};
export const ApplicationContext = React.createContext<AccountContext>(null);
interface ProviderProps {
children: React.ReactNode
}
export const ApplicationContextProvider = ({ children }: ProviderProps) => {
const [currentAccount, setCurrentAccount] = useState<Account>(null);
useEffect(() => {
checkLogin();
}, [currentAccount]);
const checkLogin = async () => {
const token = localStorage.getItem("session");
if (token) {
const token = localStorage.getItem("session");
const decode = jwt(token);
const query = {
id: decode["id"]
}
const response: Account = await api.get("/auth/account", query);
setCurrentAccount(response);
} else {
setCurrentAccount(null);
}
};
const stateValues = {
currentAccount,
setCurrentAccount,
checkLogin
};
return (
<ApplicationContext.Provider value={stateValues}>
{children}
</ApplicationContext.Provider>
);
Can someone tell me what is wrong with my context logic to authentication user to application?
Thanks for any help!
I already know what's going on, maybe it will be useful to someone in the future.
I have to add at two important things. For the first useEffect() function need to have an empty dependency array like below:
useEffect(() => {
checkLogin();
}, [ ]);
and for second I have to add also state to store loading like below:
const [loading, setLoading] = useState(false)
and set correct values when i've got data from api and so on

How can I Autogenerate tabs and also the contents dynamically from firebase realtime database using Javascript

Hello I am new to react native and cant figure out this problem please help me I am working in a project .In my project I want all the tabs and tab content to be autogenerated dynamically from my firebase database
I need that all these nodes (Living room, kitchen, bedroom, etc.) to be the names of the tab and all tabs should show its own content directly from firebase like Living room tab will show app_1 and app_2 similarly Kitchen and bedroom will also autogenerate these directly from database
Like this image:
const HorScrollView = () => {
const [homeId, setHomeId] = useState(0);
const [roomList, setRoomList] =useState(["Loading Rooms...."]);
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve,reject) => {
database().ref(`/USERS/${user.uid}/home_id`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
};
const roomListProvider = ()=>{
return new Promise((resolve,reject) => {
database().ref(`/HOMES/${homeId}/rooms`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
}
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
return (
<View style={styles.scrollViewContainer} >
<ScrollView horizontal>
{roomList.map((roomlist) => (
<Pressable key={roomlist}>
<Text style={styles.scrollViewText} >{roomlist}
</Text>
</Pressable>
))}
</ScrollView>
</View>
);
};
There's a UI library called antd which can help you do this. Use a <Table/> tag for this, then as props pass the value of the columns attribute to be the titles that are coming through, and then the dataSource attribute will be the data under those specific columns. Read the documentation for more.
While you can use the <Tabs> component from antd to achieve what you want, your current code has some bugs regarding how it handles user state and the asynchronous calls.
Taking a look at these lines:
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve, reject) => {
database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => {
resolve(snapshot.val());
});
});
};
Here there are two main problems:
You make use of auth().currentUser but this isn't guaranteed to contain the user object that you expect as it may still be resolving with the server (where it will be null) or the user may be signed out (also null).
You incorrectly chain the promise by wrapping it in a Promise constructor (known as the Promise constructor anti-pattern) where the errors of the original promise will never reach the reject handler leading to crashes.
To fix the user state problem, you should make use of onAuthStateChanged and look out for when the user signs in/out/etc.
function useCurrentUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
// returns [firebase.auth.User | null, boolean]
return [user || null, userLoading];
}
// in your component
const [user, userLoading] = useCurrentUser();
To fix the PCAPs, you'd use:
const homeidProvider = () => {
return database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const roomListProvider = () => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
Because these functions don't depend on state changes, you should place them outside your component and pass the relevant arguments into them.
Next, these lines should be inside a useEffect call where error handling and unmounting the component should be handled as appropriate:
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
should be swapped out with:
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
return;
}
let disposed = false;
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
You should also track the status of your component:
Status
Meaning
"loading"
data is loading
"error"
something went wrong
"signed-out"
no user logged in
"ready"
data is ready for display
Rolling this together:
import { Tabs, Spin, Alert, Card } from 'antd';
const { TabPane } = Tabs;
const { Meta } = Card;
function useUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
return [user || null, userLoading];
}
const getUserHomeId = (uid) => {
return database()
.ref(`/USERS/${uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const getHomeRoomData = (homeId) => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
const RoomView = () => {
const [homeId, setHomeId] = useState(0);
const [status, setStatus] = useState("loading")
const [roomList, setRoomList] = useState([]);
const [user, userLoading] = useUser();
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
setStatus("signed-out");
return;
}
let disposed = false;
setStatus("loading");
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
setStatus("ready");
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
setStatus("error");
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
switch (status) {
case "loading":
return <Spin tip="Loading rooms..." />
case "error":
return <Alert
message="Error"
description="An unknown error has occurred"
type="error"
/>
case "signed-out":
return <Alert
message="Error"
description="User is signed out"
type="error"
/>
}
if (roomList.length === 0) {
return <Alert
message="No rooms found"
description="You haven't created any rooms yet"
type="info"
/>
}
return (
<Tabs defaultActiveKey={roomList[0].key}>
{
roomList.map(room => {
let tabContent;
if (room.apps.length == 0) {
tabContent = "No apps found in this room";
} else {
tabContent = room.apps.map(app => {
<Card style={{ width: 300, marginTop: 16 }} key={app.key}>
<Meta
avatar={
<Avatar src="https://via.placeholder.com/300x300?text=Icon" />
}
title={app.name}
description={app.description}
/>
</Card>
});
}
return <TabPane tab={room.title} key={room.key}>
{tabContent}
</TabPane>
})
}
</Tabs>
);
};

How to implement microsoft oauth in a react app that uses hash router

I am trying to implement microsoft oauth button in react that redirects/pop up allows the user to sign into their microsoft account then use the info to get the access token to then send to graph api to get user info to login. I'm using this package https://www.npmjs.com/package/msal and tried registering a url for the app to redirect to.
Problem is the react app is using hash router to lazy load pages, and in order to register the redirect url in Azure AAD it can't be a fragment. So I tried switching to Browser router and upon testing it in local host atleast got results from a successful redirect.
Then trying it in a production build could'nt get it to redirect successfully. Every time it redirected, refreshed or even wrote a different path in the url it could not find the page. I read about this issue from here React-router urls don't work when refreshing or writing manually. But now not sure how to go about fixing this. I'm fairly new to coding so any kind of help of suggestion would be appreciated.
import React, { Component } from 'react';
import { BrowserRouter,browserHistory,Router, Route, Switch } from 'react-router-dom';
import { ProtectedRoute } from './auth/ProtectedRoute'
const loading = () => <div className="animated fadeIn pt-3 text-center">Loading...</div>;
// Containers
const DefaultLayout = React.lazy(() => import('./containers/DefaultLayout'));
// Pages
const Login = React.lazy(() => import('./views/Pages/Login'));
const Register = React.lazy(() => import('./views/Pages/Register'));
const Page404 = React.lazy(() => import('./views/Pages/Page404'));
const Page500 = React.lazy(() => import('./views/Pages/Page500'));
//const EditInfo = React.lazy(() => import('./views/Pages/EditInfo'));
export class App extends Component {
render() {
return (
<BrowserRouter>
<React.Suspense fallback={loading()}>
<Switch>
<Route exact path="/login" name="Login Page" render={props => <Login {...props}/>} />
<Route exact path="/register" name="Register Page" render={props => <Register {...props}/>} />
<Route exact path="/404" name="Page 404" render={props => <Page404 {...props}/>} />
<Route exact path="/500" name="Page 500" render={props => <Page500 {...props}/>} />
<ProtectedRoute path="/" name="Home" component={DefaultLayout} />
<ProtectedRoute path="/dashboard" name="Home" component={DefaultLayout} />
</Switch>
</React.Suspense>
</BrowserRouter>
);
}
}
My function to handle the configs, redirect, requests. I'm looking to redirect to the login page. However in the production build using browser router redirects to a page that won't be found on the server.
import * as Msal from 'msal';
import axios from 'axios'
export function loginOauth () {
var msalConfig = {
auth: {
clientId: 'my client id',
redirectUri: 'http://mysite.io/login'
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: true
}
};
let loginRequest = {
scopes: ["user.read"],
prompt: 'select_account'
}
let accessTokenRequest = {
scopes: ["user.read","profile"]
}
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
var msalInstance = new Msal.UserAgentApplication(msalConfig);
let authRedirectCallBack = (errorDesc, token, error, tokenType) => {
if (token) {
console.log(token);
}
else {
console.log(error + ":" + errorDesc);
}
};
msalInstance.handleRedirectCallback(authRedirectCallBack);
let signIn = () => {
msalInstance.loginRedirect(loginRequest).then(async function (loginResponse) {
return msalInstance.acquireTokenSilent(accessTokenRequest);
}).then(function (accessTokenResponse) {
const token = accessTokenResponse.accessToken;
console.log(token);
}).catch(function (error) {
//handle error
});
}
let acquireTokenRedirectAndCallMSGraph = () => {
//Always start with acquireTokenSilent to obtain a token in the signed in user from cache
msalInstance.acquireTokenSilent(accessTokenRequest).then(function (tokenResponse) {
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
// Upon acquireTokenSilent failure (due to consent or interaction or login required ONLY)
// Call acquireTokenRedirect
if (requiresInteraction(error.errorCode)) {
msalInstance.acquireTokenRedirect(accessTokenRequest);
}
});
}
let callMSGraph = (theUrl, accessToken, callback) => {
console.log(accessToken);
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200)
callback(JSON.parse(this.responseText));
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xmlHttp.send();
}
let graphAPICallback = (data) => {
console.log(data);
}
let requiresInteraction = (errorCode)=> {
if (!errorCode || !errorCode.length) {
return false;
}
return errorCode === "consent_required" ||
errorCode === "interaction_required" ||
errorCode === "login_required";
}
signIn();
}
On redirect and onloading of the page I am using the componentwillmount to get the data from the sessions.
export default class Login extends Component{
constructor(props) {
super(props);
this.state = {
modal: false,
};
this.toggle = this.toggle.bind(this);
}
toggle() {
this.setState(prevState => ({
modal: !prevState.modal,
}));
}
handleLogout(){
auth.logout(() => {console.log("logged out")})
this.toggle()
}
componentWillMount() {
var msalConfig = {
auth: {
clientId: 'my_clientid',
redirectUri: 'http://mysite.io/login'
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: true
}
};
var msalInstance = new Msal.UserAgentApplication(msalConfig);
if (msalInstance.getAccount()) {
var tokenRequest = {
scopes: ["user.read"]
};
msalInstance.acquireTokenSilent(tokenRequest)
.then(response => {
callMSGraph(response.accessToken, (data)=>console.log(data))
// get access token from response
// response.accessToken
})
.catch(err => {
// could also check if err instance of InteractionRequiredAuthError if you can import the class.
if (err.name === "InteractionRequiredAuthError") {
return msalInstance.acquireTokenPopup(tokenRequest)
.then(response => {
// get access token from response
// response.accessToken
})
.catch(err => {
// handle error
});
}
});
} else {
// user is not logged in, you will need to log them in to acquire a token
}
let token = sessionStorage.getItem('msal.idtoken');
if(token !== null) {
var decoded = jwt_decode(token);
console.log(decoded);
} else {
console.log("NO token yet");
}
}
render() {
let open;
if(auth.isAuthenticated() === "true"){
open = true
}else{ open = false}
return (<div><button onClick={ () => {loginOauth()}} >Login with Microsoft</button> </div>);
}
You should only register the base URL you want the user redirected to. To allow your state data (i.e. your fragment) to traverse the service boundaries, you use the state parameter.
Any information you pass into the OAuth via the state parameter will be returned with your token. You then parse the state when the user is returned.

Update apollo client with response of mutation

I have a login component that when submitted, calls a login mutation and retrieves the user data.
When the user data is returned, I want to set the apollo client state but I am confused by the docs on how to do so:
My component looks as so:
const LOGIN = gql`
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
username
fullName
}
}
`
const Login = () => {
const onSubmit = (data, login) => {
login({ variables: data })
.then(response => console.log("response", response))
.catch(err => console.log("err", err))
}
return (
<Mutation
mutation={LOGIN}
update={(cache, { data: { login } }) => {
}}
>
{(login, data) => {
return (
<Fragment>
{data.loading ? (
<Spinner />
) : (
<Form buttonLabel="Submit" fields={loginForm} onChange={() => {}} onSubmit={e => onSubmit(e, login)} />
)}
{data.error ? <div>Incorrect username or password</div> : null}
</Fragment>
)
}}
</Mutation>
)
}
As you can see, I have the update prop in my mutation which receives the login param and has the user data which I want to set in the apollo client's state.
The example here writes the response to the cache cache.writeQuery but I am unsure if this is what I want to do. Would I not want to write to client (as opposed to cache) like they do in this example where they update the local data?
The update property of mutation only seems to receive the cache param so I'm not sure if there is any way to access client as opposed to cache.
How do I go about updating my apollo client state with the response of my mutation in the update property of mutate?
EDIT: my client:
const cache = new InMemoryCache()
const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
clientState: {
defaults: {
locale: "en-GB",
agent: null /* <--- update agent */
},
typeDefs: `
enum Locale {
en-GB
fr-FR
nl-NL
}
type Query {
locale: Locale
}
`
},
cache
})
Using Context API
If you have wrapped your component somewhere higher up in the hierarchy with ApolloProvider
Using ApolloConsumer
const Login = () => {
const onSubmit = async (data, login, client) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<ApolloConsumer>
{client => (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login, client)} />}
</Mutation>
)}
</ApolloConsumer>
);
};
Using withApollo
const Login = client => {
const onSubmit = async (data, login) => {
const response = await login({ variables: data });
if (response) {
client.whatever = response;
}
};
return (
<Mutation mutation={LOGIN}>
{login => <Form onSubmit={e => onSubmit(e, login)} />}
</Mutation>
);
};
withApollo(Login);
Without Context API
import { client } from "wherever-you-made-your-client";
and just reference it there

Categories