Skip to content

Commit 81f8fc1

Browse files
authored
Merge pull request #11 from streamich/feat-auth
Feat auth
2 parents 6b2a4ca + 9a1dd99 commit 81f8fc1

File tree

14 files changed

+12465
-53
lines changed

14 files changed

+12465
-53
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ node_modules/
44
/.nyc_output/
55
/coverage/
66
package-lock.json
7-
yarn.lock
87
/lib/
98
/modules/
109
/.vscode/

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const MyComponent = mock();
7171
- [`<WindowSizeSensor>`](./docs/en/WindowSizeSensor.md), [`withWindowSize()`](./docs/en/WindowSizeSensor.md#withwindowsize-hoc), and [`@withWindowSize`](./docs/en/WindowSizeSensor.md#withwindowsize-decorator)
7272
- [`<WindowWidthSensor>`](./docs/en/WindowWidthSensor.md), [`withWindowWidth()`](./docs/en/WindowWidthSensor.md#withwindowwidth-hoc), and [`@withWindowWidth`](./docs/en/WindowWidthSensor.md#withwindowwidth-decorator)
7373
- [Context](./docs/en/Context.md)
74+
- [Google Sign-in for Websites](./docs/en/GoogleAuth.md)
7475
- [`<Provider>`](./docs/en/Provider.md#provider), [`<Consumer>`](./docs/en/Provider.md#consumer), [`withContext()`](./docs/en/Provider.md#withcontext-hoc), and [`@withContext`](./docs/en/Provider.md#withcontext-decorator)
7576
- [`<Theme>`](./docs/en/theme.md#theme), [`<Themed>`](./docs/en/theme.md#themed), [`withTheme()`](./docs/en/theme.md#withtheme-hoc), and [`@withTheme`](./docs/en/theme.md#withtheme-decorator)
7677
- [`<CssVarsProvider>`](./docs/en/cssvars.md), [`<CssVars>`](./docs/en/cssvars.md#cssvars), [`withCssVars()`](./docs/en/cssvars.md#withcssvars-hoc), and [`@withCssVars`](./docs/en/cssvars.md#withcssvars-decorator)

docs/en/GoogleAuth.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Google Sign-in for Websites
2+
3+
React wrapper for [Google Sign-In for Websites](https://developers.google.com/identity/sign-in/web/).
4+
5+
6+
## Usage
7+
8+
First use `createGoogleAuthContext` to create your context components. You need to obtain Google
9+
app `client_id` from [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin).
10+
11+
Then wrap your entire app with `<Provider>` component and anywhere in your app use the `<Consumer>` component.
12+
13+
```js
14+
import {createGoogleAuthContext} from 'libreact/lib/GoogleAuth';
15+
16+
const {Provider, Conumer} = createGoogleAuthContext({
17+
client_id: 'xxxxxxxx-yyyyyyyyyyyyyyyy.apps.googleusercontent.com',
18+
});
19+
20+
<Provider>
21+
<Consumer>{({loading, signIn, signOut, isSignedIn, user}) => {
22+
if (loading) {
23+
return 'Loading...';
24+
}
25+
console.log('user', user);
26+
return (
27+
<div>
28+
<button onClick={isSignedIn ? signOut : signIn}>
29+
{isSignedIn ? 'Log out' : 'Sign in with Google!'}
30+
</button>
31+
<div>Is signed in: {isSignedIn ? 'true' : 'false'}</div>
32+
{user &&
33+
<div>
34+
<div>Name: {user.getBasicProfile().getName()}</div>
35+
<div>JWT: {user.getAuthResponse().id_token}</div>
36+
</div>
37+
}
38+
</div>
39+
);
40+
}}</Consumer>
41+
</Provider>
42+
```

docs/en/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
- [`<WindowSizeSensor>`](./WindowSizeSensor.md), [`withWindowSize()`](./WindowSizeSensor.md#withwindowsize-hoc), and [`@withWindowSize`](./WindowSizeSensor.md#withwindowsize-decorator)
4242
- [`<WindowWidthSensor>`](./WindowWidthSensor.md), [`withWindowWidth()`](./WindowWidthSensor.md#withwindowwidth-hoc), and [`@withWindowWidth`](./WindowWidthSensor.md#withwindowwidth-decorator)
4343
- [Context](./Context.md)
44+
- [Google Sign-in for Websites](./GoogleAuth.md)
4445
- [`<Provider>`](./Provider.md#provider), [`<Consumer>`](./Provider.md#consumer), [`withContext()`](./Provider.md#withcontext-hoc), and [`@withContext`](./Provider.md#withcontext-decorator)
4546
- [`<Theme>`](./theme.md#theme), [`<Themed>`](./theme.md#themed), [`withTheme()`](./theme.md#withtheme-hoc), and [`@withTheme`](./theme.md#withtheme-decorator)
4647
- [`<CssVarsProvider>`](./cssvars.md), [`<CssVars>`](./cssvars.md#cssvars), [`withCssVars()`](./cssvars.md#withcssvars-hoc), and [`@withCssVars`](./cssvars.md#withcssvars-decorator)

docs/en/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
* [WindowSizeSensor](WindowSizeSensor.md)
5050
* [WindowWidthSensor](WindowWidthSensor.md)
5151
* [Context](Context.md)
52+
* [Google Sign-in for Websites](GoogleAuth.md)
5253
* [Provider](Provider.md)
5354
* [Theming](theme.md)
5455
* [CSS Variables](cssvars.md)

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
"enzyme-to-json": "^3.3.4",
5656
"gulp": "3.9.1",
5757
"gulp-typescript": "3",
58-
"jest": "22.1.2",
59-
"jest-environment-jsdom": "^22.1.4",
60-
"jest-environment-jsdom-global": "^1.0.3",
58+
"jest": "^23.6.0",
59+
"jest-environment-jsdom": "^23.4.0",
60+
"jest-environment-jsdom-global": "^1.1.0",
6161
"mocha": "5.0.0",
6262
"git-cz": "^1.7.0",
6363
"react-markdown": "3.1.4",
@@ -112,7 +112,8 @@
112112
"setupFiles": [
113113
"./src/__tests__/setup.js"
114114
],
115-
"testEnvironment": "jest-environment-jsdom-global"
115+
"testEnvironment": "jest-environment-jsdom-global",
116+
"testURL": "http://localhost"
116117
},
117118
"keywords": [
118119
"react",

src/GoogleAuth/__story__/story.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {createElement as h} from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {createGoogleAuthContext} from '..';
4+
import ShowDocs from '../../../.storybook/ShowDocs'
5+
6+
const clientId = '305188012168-htfit0k0u4vegn0f6hn10rcqoj1m77ca.apps.googleusercontent.com';
7+
const options = {
8+
client_id: clientId,
9+
};
10+
const ctx1 = createGoogleAuthContext(options);
11+
12+
storiesOf('Context/GoogleAuthButton', module)
13+
.add('Documentation', () => h(ShowDocs, {md: require('../../../docs/en/GoogleAuth.md')}))
14+
.add('Default', () =>
15+
<ctx1.Provider>
16+
<ctx1.Consumer>{({loading, signIn, signOut, isSignedIn, user}) => {
17+
if (loading) {
18+
return 'Loading...';
19+
}
20+
console.log('user', user);
21+
return (
22+
<div>
23+
<button onClick={isSignedIn ? signOut : signIn}>
24+
{isSignedIn ? 'Log out' : 'Sign in with Google!'}
25+
</button>
26+
<div>Is signed in: {isSignedIn ? 'true' : 'false'}</div>
27+
{user &&
28+
<div>
29+
<div>Name: {user.getBasicProfile().getName()}</div>
30+
<div>JWT: {user.getAuthResponse().id_token}</div>
31+
</div>
32+
}
33+
</div>
34+
);
35+
}}</ctx1.Consumer>
36+
</ctx1.Provider>
37+
);

src/GoogleAuth/gapi.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
export interface GApi {
2+
load(what: 'auth2', callback: () => void);
3+
auth2: GApiAuth2;
4+
}
5+
6+
export interface GApiAuth2InitOptions {
7+
client_id: string;
8+
scope?: string;
9+
}
10+
11+
export interface GApiAuth2 {
12+
init(options: GApiAuth2InitOptions): Promise<GApiAuth2Instance>;
13+
}
14+
15+
export type GApiAuth2InstanceIsSignedInListener = (isSignedIn: boolean) => void;
16+
export type GApiAuth2InstanceCurrentUserListener = (isSignedIn: GApiAuth2User) => void;
17+
export interface GApiAuth2Instance {
18+
signIn(): Promise<GApiAuth2User>;
19+
signOut(): Promise<void>;
20+
isSignedIn: {
21+
get(): boolean;
22+
listen(listener: GApiAuth2InstanceIsSignedInListener);
23+
};
24+
currentUser: {
25+
get(): GApiAuth2User;
26+
listen(listener: GApiAuth2InstanceCurrentUserListener);
27+
};
28+
}
29+
30+
export interface GApiAuth2User {
31+
getId(): string;
32+
isSignedIn(): boolean;
33+
getHostedDomain(): string;
34+
getGrantedScopes(): string;
35+
getBasicProfile(): GApiAuth2BasicProfile;
36+
getAuthResponse(): GApiAuth2AuthResponse;
37+
reloadAuthResponse(): Promise<GApiAuth2AuthResponse>;
38+
hasGrantedScopes(scopes: string): boolean;
39+
}
40+
41+
export interface GApiAuth2BasicProfile {
42+
getId(): string;
43+
getName(): string;
44+
getGivenName(): string;
45+
getFamilyName(): string;
46+
getImageUrl(): string;
47+
getEmail(): string;
48+
}
49+
50+
export interface GApiAuth2AuthResponse {
51+
access_token: string;
52+
id_token: string;
53+
scope: string;
54+
expires_in: string;
55+
first_issued_at: string;
56+
expires_at: string;
57+
}
58+
59+
let gapiCache: GApi;
60+
61+
export const getGapi = async (): Promise<GApi> => {
62+
if (gapiCache) {
63+
return gapiCache;
64+
}
65+
66+
await new Promise(resolve => {
67+
const gapicallback = `__gapicb${Date.now().toString(36)}`;
68+
(window as any)[gapicallback] = () => {
69+
delete (window as any)[gapicallback];
70+
gapiCache = (window as any).gapi;
71+
resolve();
72+
};
73+
74+
const script = document.createElement('script');
75+
76+
script.src = 'https://apis.google.com/js/platform.js?onload=' + gapicallback;
77+
document.head.appendChild(script);
78+
});
79+
80+
return gapiCache;
81+
};
82+
83+
let gapiAuth2Cache;
84+
85+
export const getGapiAuth2 = async (): Promise<GApiAuth2> => {
86+
if (gapiAuth2Cache) {
87+
return gapiAuth2Cache;
88+
}
89+
90+
const gapi = await getGapi();
91+
92+
await new Promise(resolve => {
93+
gapi.load('auth2', () => {
94+
gapiAuth2Cache = gapi.auth2;
95+
resolve();
96+
});
97+
});
98+
99+
return gapiAuth2Cache;
100+
};
101+
102+
export const getGapiAuthInstance = async (options: GApiAuth2InitOptions) => {
103+
const gapiAuth2 = await getGapiAuth2();
104+
return await gapiAuth2.init(options);
105+
};

src/GoogleAuth/index.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {h} from '../util';
2+
import {Component, createContext} from 'react';
3+
import {
4+
getGapiAuthInstance,
5+
GApiAuth2InitOptions,
6+
GApiAuth2Instance,
7+
GApiAuth2InstanceIsSignedInListener,
8+
GApiAuth2InstanceCurrentUserListener,
9+
GApiAuth2User
10+
} from './gapi';
11+
12+
export interface IGoogleAuthProviderProps {
13+
children;
14+
}
15+
16+
export interface IGoogleAuthProviderState {
17+
loading: boolean;
18+
signIn: () => Promise<GApiAuth2User>;
19+
signOut: () => Promise<void>;
20+
user: GApiAuth2User | null;
21+
isSignedIn: boolean;
22+
}
23+
24+
export interface IGoogleAuthConsumerProps {
25+
children: (state: IGoogleAuthProviderState) => React.ReactNode;
26+
}
27+
28+
/**
29+
* @param clientId Google App client ID.
30+
* @param scope Scopes as a comma separated string.
31+
*/
32+
export const createGoogleAuthContext = (options: GApiAuth2InitOptions) => {
33+
const context = createContext({});
34+
const googleAuthPromise = getGapiAuthInstance(options);
35+
let googleAuthInstance: GApiAuth2Instance | undefined;
36+
let isSignedInListener: GApiAuth2InstanceIsSignedInListener | undefined;
37+
let currentUserListener: GApiAuth2InstanceCurrentUserListener | undefined;
38+
39+
googleAuthPromise.then((instance) => {
40+
googleAuthInstance = instance;
41+
googleAuthInstance.isSignedIn.listen((isSignedIn) => {
42+
if (isSignedInListener) isSignedInListener(isSignedIn);
43+
});
44+
googleAuthInstance.currentUser.listen((user) => {
45+
if (currentUserListener) currentUserListener(user);
46+
});
47+
}, console.error);
48+
49+
class Provider extends Component<IGoogleAuthProviderProps, IGoogleAuthProviderState> {
50+
signIn = async (): Promise<GApiAuth2User> => {
51+
if (!googleAuthInstance) {
52+
googleAuthInstance = await googleAuthPromise;
53+
}
54+
55+
return await googleAuthInstance.signIn();
56+
};
57+
58+
signOut = async (): Promise<void> => {
59+
if (!googleAuthInstance) {
60+
googleAuthInstance = await googleAuthPromise;
61+
}
62+
63+
await googleAuthInstance.signOut();
64+
};
65+
66+
onIsSignedIn: GApiAuth2InstanceIsSignedInListener = (isSignedIn) => {
67+
if (isSignedIn) {
68+
// Don't do anything here, because this case will be handled by
69+
// this.onCurrentUser method. To prevent double re-render.
70+
} else {
71+
this.setState({
72+
isSignedIn: false,
73+
user: null,
74+
});
75+
}
76+
};
77+
78+
onCurrentUser: GApiAuth2InstanceCurrentUserListener = (user) => {
79+
// Only handle the case when user signs in. The other case should
80+
// be handled by this.onIsSignedIn. To prevent double re-render.
81+
if (user.isSignedIn()) {
82+
this.setState({
83+
isSignedIn: true,
84+
user
85+
});
86+
}
87+
};
88+
89+
state: IGoogleAuthProviderState = {
90+
loading: !googleAuthInstance,
91+
signIn: this.signIn,
92+
signOut: this.signOut,
93+
user: null,
94+
isSignedIn: false,
95+
};
96+
97+
async componentDidMount () {
98+
if (!googleAuthInstance) {
99+
googleAuthInstance = await googleAuthPromise;
100+
}
101+
102+
isSignedInListener = this.onIsSignedIn;
103+
currentUserListener = this.onCurrentUser;
104+
105+
const isSignedIn = googleAuthInstance.isSignedIn.get();
106+
107+
this.setState({
108+
loading: false,
109+
isSignedIn,
110+
user: isSignedIn ? googleAuthInstance.currentUser.get() : null,
111+
});
112+
}
113+
114+
componentWillUnmount () {
115+
if (isSignedInListener === this.onIsSignedIn)
116+
isSignedInListener = undefined;
117+
if (currentUserListener === this.onCurrentUser)
118+
currentUserListener = undefined;
119+
}
120+
121+
render () {
122+
return h(context.Provider, {value: this.state}, this.props.children);
123+
}
124+
}
125+
126+
return {
127+
Provider,
128+
Consumer: context.Consumer as React.SFC<IGoogleAuthConsumerProps>,
129+
};
130+
};

src/LocalStorage/__tests__/__snapshots__/index.test.tsx.snap

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)