Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

(examples) - With-refresh-auth (#1605)

* Add With-refresh-auth example

Changed files
+335 -4
examples
with-graphcache-pagination
with-multipart
with-refresh-auth
+2 -1
examples/with-graphcache-pagination/README.md
···
-
# Integrating with React
+
# Integrating with `@urql/exchange-graphcache`'s cacheExchange pagination
Integrating urql is as simple as:
···
```
2. Install [graphcache](https://formidable.com/open-source/urql/docs/graphcache/)
+
```sh
yarn add @urql/exchange-graphcache
# or
+1 -1
examples/with-multipart/README.md
···
-
# Integrating with React
+
# Integrating with `@urql/exchange-multipart-fetch`'s multipartFetchExchange
Integrating urql is as simple as:
+21
examples/with-refresh-auth/README.md
···
+
# Integrating with `@urql/exchange-auth`'s authExchange
+
+
Integrating urql is as simple as:
+
+
1. Install packages [getting started](https://formidable.com/open-source/urql/docs/basics/react-preact/)
+
+
```sh
+
yarn add urql graphql
+
# or
+
npm install --save urql graphql
+
```
+
+
2. Install [authExchange](https://formidable.com/open-source/urql/docs/advanced/authentication/)
+
+
```sh
+
yarn add @urql/exchange-auth
+
# or
+
npm install --save @urql/exchange-auth
+
```
+
+
3. Setting up the Client and setup the Auth flow [here](src/App.js)
+39
examples/with-refresh-auth/package.json
···
+
{
+
"name": "react-urql-query",
+
"version": "1.0.0",
+
"private": true,
+
"dependencies": {
+
"@urql/exchange-auth": "^0.1.2",
+
"graphql": "^15.5.0",
+
"react": "^17.0.2",
+
"react-dom": "^17.0.2",
+
"urql": "^2.0.2"
+
},
+
"devDependencies": {
+
"react-scripts": "4.0.3"
+
},
+
"scripts": {
+
"start": "react-scripts start",
+
"build": "react-scripts build",
+
"test": "react-scripts test",
+
"eject": "react-scripts eject"
+
},
+
"eslintConfig": {
+
"extends": [
+
"react-app",
+
"react-app/jest"
+
]
+
},
+
"browserslist": {
+
"production": [
+
">0.2%",
+
"not dead",
+
"not op_mini all"
+
],
+
"development": [
+
"last 1 chrome version",
+
"last 1 firefox version",
+
"last 1 safari version"
+
]
+
}
+
}
+15
examples/with-refresh-auth/src/App.js
···
+
import React from 'react';
+
import { Provider } from 'urql';
+
+
import client from './client';
+
import Home from './pages/Home';
+
+
function App() {
+
return (
+
<Provider value={client}>
+
<Home />
+
</Provider>
+
);
+
}
+
+
export default App;
+19
examples/with-refresh-auth/src/auth/Store.js
···
+
const TOKEN_KEY = 'token';
+
const REFRESH_TOKEN_KEY = 'refresh_token';
+
+
export const saveAuthData = ({ token, refreshToken }) => {
+
localStorage.setItem(TOKEN_KEY, token);
+
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
+
};
+
+
export const getToken = () => {
+
return localStorage.getItem(TOKEN_KEY);
+
};
+
+
export const getRefreshToken = () => {
+
return localStorage.getItem(REFRESH_TOKEN_KEY);
+
};
+
+
export const clearStorage = () => {
+
localStorage.clear();
+
};
+91
examples/with-refresh-auth/src/client/index.js
···
+
import {
+
createClient,
+
dedupExchange,
+
fetchExchange,
+
cacheExchange,
+
gql,
+
} from 'urql';
+
import { makeOperation } from '@urql/core';
+
import { authExchange } from '@urql/exchange-auth';
+
import {
+
getRefreshToken,
+
getToken,
+
saveAuthData,
+
clearStorage,
+
} from '../auth/Store';
+
+
const REFRESH_TOKEN_MUTATION = gql`
+
mutation RefreshCredentials($refreshToken: String!) {
+
refreshCredentials(refreshToken: $refreshToken) {
+
refreshToken
+
token
+
}
+
}
+
`;
+
+
const getAuth = async ({ authState, mutate }) => {
+
if (!authState) {
+
const token = getToken();
+
const refreshToken = getRefreshToken();
+
+
if (token && refreshToken) {
+
return { token, refreshToken };
+
}
+
+
return null;
+
}
+
+
const result = await mutate(REFRESH_TOKEN_MUTATION, {
+
refreshToken: authState.refreshToken,
+
});
+
+
if (result.data?.refreshCredentials) {
+
saveAuthData(result.data.refreshCredentials);
+
+
return result.data.refreshCredentials;
+
}
+
+
// This is where auth has gone wrong and we need to clean up and redirect to a login page
+
clearStorage();
+
window.location.reload();
+
+
return null;
+
};
+
+
const addAuthToOperation = ({ authState, operation }) => {
+
if (!authState || !authState.token) {
+
return operation;
+
}
+
+
const fetchOptions =
+
typeof operation.context.fetchOptions === 'function'
+
? operation.context.fetchOptions()
+
: operation.context.fetchOptions || {};
+
+
return makeOperation(operation.kind, operation, {
+
...operation.context,
+
fetchOptions: {
+
...fetchOptions,
+
headers: {
+
...fetchOptions.headers,
+
Authorization: `Bearer ${authState.token}`,
+
},
+
},
+
});
+
};
+
+
const didAuthError = ({ error }) => {
+
return error.graphQLErrors.some(e => e.extensions?.code === 'UNAUTHORIZED');
+
};
+
+
const client = createClient({
+
url: 'https://trygql.dev/graphql/web-collections',
+
exchanges: [
+
dedupExchange,
+
cacheExchange({}),
+
authExchange({ getAuth, addAuthToOperation, didAuthError }),
+
fetchExchange,
+
],
+
});
+
+
export default client;
+13
examples/with-refresh-auth/src/index.css
···
+
body {
+
margin: 0;
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+
sans-serif;
+
-webkit-font-smoothing: antialiased;
+
-moz-osx-font-smoothing: grayscale;
+
}
+
+
code {
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+
monospace;
+
}
+12
examples/with-refresh-auth/src/index.js
···
+
import React from 'react';
+
import ReactDOM from 'react-dom';
+
import './index.css';
+
import App from './App';
+
+
ReactDOM.render(
+
<React.StrictMode>
+
<App />
+
</React.StrictMode>,
+
document.getElementById('root')
+
);
+
+28
examples/with-refresh-auth/src/pages/Home.js
···
+
import React, { useEffect, useState } from 'react';
+
+
import { getToken, saveAuthData } from '../auth/Store';
+
import Profile from './Profile';
+
import LoginForm from './LoginForm';
+
+
const Home = () => {
+
const [isLoggedIn, setIsLoggedIn] = useState(false);
+
+
const onLoginSuccess = auth => {
+
setIsLoggedIn(true);
+
saveAuthData(auth);
+
};
+
+
useEffect(() => {
+
if (getToken()) {
+
setIsLoggedIn(true);
+
}
+
}, []);
+
+
return isLoggedIn ? (
+
<Profile />
+
) : (
+
<LoginForm onLoginSuccess={onLoginSuccess} />
+
);
+
};
+
+
export default Home;
+57
examples/with-refresh-auth/src/pages/LoginForm.js
···
+
import React, { useEffect, useState } from 'react';
+
import { gql, useMutation } from 'urql';
+
+
const LOGIN_MUTATION = gql`
+
mutation Signin($input: LoginInput!) {
+
signin(input: $input) {
+
refreshToken
+
token
+
}
+
}
+
`;
+
+
const LoginForm = ({ onLoginSuccess }) => {
+
const [password, setPassword] = useState('');
+
const [username, setUsername] = useState('');
+
+
const [loginResult, login] = useMutation(LOGIN_MUTATION);
+
+
const { data, fetching, error } = loginResult;
+
+
const onUsernameChange = evt => {
+
setUsername(evt.target.value);
+
};
+
+
const onPasswordChange = evt => {
+
setPassword(evt.target.value);
+
};
+
+
const onSubmit = evt => {
+
evt.preventDefault();
+
login({ input: { username, password } });
+
};
+
+
useEffect(() => {
+
if (data && data.signin) {
+
onLoginSuccess(data.signin);
+
}
+
}, [onLoginSuccess, data]);
+
+
if (fetching) {
+
return <p>loading...</p>;
+
}
+
+
return (
+
<form onSubmit={onSubmit}>
+
{error && <p>Oh no... {error.message}</p>}
+
+
<input type="text" onChange={onUsernameChange} />
+
+
<input type="password" onChange={onPasswordChange} />
+
+
<input type="submit" title="login" />
+
</form>
+
);
+
};
+
+
export default LoginForm;
+36
examples/with-refresh-auth/src/pages/Profile.js
···
+
import React from 'react';
+
import { gql, useQuery } from 'urql';
+
+
const PROFILE_QUERY = gql`
+
query Profile {
+
me {
+
id
+
username
+
createdAt
+
}
+
}
+
`;
+
+
const Profile = () => {
+
const [result] = useQuery({ query: PROFILE_QUERY });
+
+
const { data, fetching, error } = result;
+
+
return (
+
<div>
+
{fetching && <p>Loading...</p>}
+
+
{error && <p>Oh no... {error.message}</p>}
+
+
{data && (
+
<>
+
<p>profile data</p>
+
<p>id: {data.me.id}</p>
+
<p>username: {data.me.username}</p>
+
</>
+
)}
+
</div>
+
);
+
};
+
+
export default Profile;
+1 -2
package.json
···
},
"eslintIgnore": [
"packages/site/dist-prod",
-
"docs",
-
"examples"
+
"docs"
],
"prettier": {
"singleQuote": true,