Skip to content

Commit ece3676

Browse files
committed
feat: 🎸 implement Google auth Provider and Consumer components
1 parent 00fbdb5 commit ece3676

File tree

4 files changed

+214
-68
lines changed

4 files changed

+214
-68
lines changed

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,34 @@ import {createGoogleAuthContext} from '..';
44
// import ShowDocs from '../../../.storybook/ShowDocs'
55

66
const clientId = '305188012168-htfit0k0u4vegn0f6hn10rcqoj1m77ca.apps.googleusercontent.com';
7-
const ctx1 = createGoogleAuthContext(clientId);
7+
const options = {
8+
client_id: clientId,
9+
};
10+
const ctx1 = createGoogleAuthContext(options);
811

912
storiesOf('Context/GoogleAuthButton', module)
1013
// .add('Documentation', () => h(ShowDocs, {md: require('../../../docs/en/GoogleAuthButton.md')}))
1114
.add('Default', () =>
1215
<ctx1.Provider>
13-
<ctx1.Consumer>{({signIn}) =>
14-
<button onClick={signIn}>Sign in with Google!</button>
15-
}</ctx1.Consumer>
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>
1636
</ctx1.Provider>
1737
);

‎src/GoogleAuth/gapi.ts‎

Lines changed: 91 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,107 @@
1-
export type GoogleAPI = any;
2-
3-
let gapiCache: GoogleAPI;
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+
}
465

5-
export const getGapi = async (): Promise<GoogleAPI> => {
6-
if (gapiCache) {
7-
return gapiCache;
8-
}
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+
};
973

10-
await new Promise(resolve => {
11-
const gapicallback = `__gapicb${Date.now().toString(36)}`;
12-
(window as any)[gapicallback] = () => {
13-
delete (window as any)[gapicallback];
14-
gapiCache = (window as any).gapi;
15-
resolve();
16-
};
74+
const script = document.createElement('script');
1775

18-
const script = document.createElement('script');
76+
script.src = 'https://apis.google.com/js/platform.js?onload=' + gapicallback;
77+
document.head.appendChild(script);
78+
});
1979

20-
script.src = 'https://apis.google.com/js/platform.js?onload=' + gapicallback;
21-
document.head.appendChild(script);
22-
});
23-
24-
return gapiCache;
80+
return gapiCache;
2581
};
2682

2783
let gapiAuth2Cache;
2884

29-
export const getGapiAuth2 = async (): Promise<any> => {
30-
if (gapiAuth2Cache) {
31-
return gapiAuth2Cache;
32-
}
85+
export const getGapiAuth2 = async (): Promise<GApiAuth2> => {
86+
if (gapiAuth2Cache) {
87+
return gapiAuth2Cache;
88+
}
3389

34-
const gapi = await getGapi();
90+
const gapi = await getGapi();
3591

36-
await new Promise(resolve => {
37-
gapi.load('auth2', () => {
38-
gapiAuth2Cache = gapi.auth2;
39-
resolve();
40-
});
92+
await new Promise(resolve => {
93+
gapi.load('auth2', () => {
94+
gapiAuth2Cache = gapi.auth2;
95+
resolve();
4196
});
97+
});
4298

43-
return gapiAuth2Cache;
99+
return gapiAuth2Cache;
44100
};
45101

46-
let GoogleAuthCache;
47-
48-
export const getGapiAuthInstance = async (client_id: string) => {
49-
if (GoogleAuthCache) {
50-
return GoogleAuthCache;
51-
}
52-
53-
const gapiAuth2 = await getGapiAuth2();
54-
55-
GoogleAuthCache = await gapiAuth2.init({client_id});
102+
export const getGapiAuthInstance = async (options: GApiAuth2InitOptions) => {
103+
const gapiAuth2 = await getGapiAuth2();
104+
const GoogleAuthCache = await gapiAuth2.init(options);
56105

57-
return GoogleAuthCache;
106+
return GoogleAuthCache;
58107
};

‎src/GoogleAuth/index.ts‎

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,123 @@
11
import {h} from '../util';
22
import {Component, createContext} from 'react';
3-
import {getGapiAuthInstance} from './gapi';
3+
import {
4+
getGapiAuthInstance,
5+
GApiAuth2InitOptions,
6+
GApiAuth2Instance,
7+
GApiAuth2InstanceIsSignedInListener,
8+
GApiAuth2InstanceCurrentUserListener,
9+
GApiAuth2User
10+
} from './gapi';
411

512
export interface IGoogleAuthProviderProps {
613
children;
714
}
815

916
export interface IGoogleAuthProviderState {
10-
loading?: boolean;
11-
signIn: () => Promise<void>;
17+
loading: boolean;
18+
signIn: () => Promise<GApiAuth2User>;
1219
signOut: () => Promise<void>;
13-
user: any | null;
20+
user: GApiAuth2User | null;
21+
isSignedIn: boolean;
1422
}
1523

1624
export interface IGoogleAuthConsumerProps {
1725
children: (state: IGoogleAuthProviderState) => React.ReactNode;
1826
}
1927

20-
export const createGoogleAuthContext = (clientId: string) => {
28+
/**
29+
* @param clientId Google App client ID.
30+
* @param scope Scopes as a comma separated string.
31+
*/
32+
export const createGoogleAuthContext = (options: GApiAuth2InitOptions) => {
2133
const context = createContext({});
34+
const googleAuthPromise = getGapiAuthInstance(options);
35+
let googleAuthInstance: GApiAuth2Instance | undefined;
36+
let isSignedInListener: GApiAuth2InstanceIsSignedInListener | undefined;
37+
let currentUserListener: GApiAuth2InstanceCurrentUserListener | undefined;
2238

23-
let googleAuth;
24-
const getAuthInstance = async () => {
25-
if (!googleAuth) {
26-
googleAuth = await getGapiAuthInstance(clientId);
27-
}
28-
29-
return googleAuth;
30-
};
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);
3148

3249
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+
3389
state: IGoogleAuthProviderState = {
34-
signIn: async () => {
35-
const googleAuth = await getAuthInstance();
36-
await googleAuth.signIn();
37-
},
38-
signOut: async () => {
39-
const googleAuth = await getAuthInstance();
40-
await googleAuth.signOut();
41-
},
90+
loading: !googleAuthInstance,
91+
signIn: this.signIn,
92+
signOut: this.signOut,
4293
user: null,
94+
isSignedIn: false,
4395
};
4496

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+
45121
render () {
46122
return h(context.Provider, {value: this.state}, this.props.children);
47123
}

0 commit comments

Comments
 (0)