Skip to content

Commit b69f231

Browse files
authored
Merge pull request #30 from ljosberinn/master
recoverAccount workflow... and a bit more
2 parents 155257e + c33c658 commit b69f231

File tree

5 files changed

+2935
-2330
lines changed

5 files changed

+2935
-2330
lines changed

package.json

+12-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"dependencies": {
2727
"gotrue-js": "^0.9.25",
28-
"tsdx": "^0.6.0"
28+
"tsdx": "^0.12.3"
2929
},
3030
"peerDependencies": {
3131
"react": "^15.0.0 || ^16.0.0",
@@ -43,18 +43,18 @@
4343
"trailingComma": "es5"
4444
},
4545
"devDependencies": {
46-
"@svgr/rollup": "^4.2.0",
47-
"@types/jest": "^23.1.5",
48-
"@types/react": "^16.7.13",
49-
"@types/react-dom": "^16.0.11",
50-
"auto-changelog": "^1.13.0",
46+
"@svgr/rollup": "^5.0.1",
47+
"@types/jest": "^24.0.25",
48+
"@types/react": "^16.9.17",
49+
"@types/react-dom": "^16.9.4",
50+
"auto-changelog": "^1.16.2",
5151
"gh-release": "^3.5.0",
52-
"husky": "^2.3.0",
53-
"prettier": "^1.17.1",
54-
"pretty-quick": "^1.11.0",
55-
"react": "^16.8.6",
56-
"react-dom": "^16.8.6",
57-
"typescript": "^3.5.1"
52+
"husky": "^4.0.9",
53+
"prettier": "^1.19.1",
54+
"pretty-quick": "^2.0.1",
55+
"react": "^16.12.0",
56+
"react-dom": "^16.12.0",
57+
"typescript": "^3.7.4"
5858
},
5959
"files": [
6060
"dist"

src/index.tsx

+165-70
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
1-
import React from 'react';
1+
import React, {
2+
useState,
3+
useMemo,
4+
useEffect,
5+
createContext,
6+
useContext,
7+
// types
8+
Dispatch,
9+
SetStateAction,
10+
ReactNode,
11+
useCallback,
12+
} from 'react';
213

3-
import GoTrue, { User, Settings } from 'gotrue-js';
14+
import GoTrue, {
15+
User as GoTrueUser,
16+
Settings as GoTrueSettings,
17+
} from 'gotrue-js';
418
import { runRoutes } from './runRoutes';
19+
import { TokenParam, defaultParam } from './token';
520

621
type authChangeParam = (user?: User) => string | void;
722

8-
export type Settings = Settings;
9-
export type User = User;
23+
export type Settings = GoTrueSettings;
24+
export type User = GoTrueUser;
25+
type Provider = 'bitbucket' | 'github' | 'gitlab' | 'google';
1026

1127
const defaultSettings = {
1228
autoconfirm: false,
@@ -21,29 +37,34 @@ const defaultSettings = {
2137
},
2238
};
2339

40+
const errors = {
41+
noUserFound: 'No current user found - are you logged in?',
42+
noUserTokenFound: 'no user token found',
43+
tokenMissingOrInvalid: 'either no token found or invalid for this purpose',
44+
};
45+
46+
type MaybeUserPromise = Promise<User | undefined>;
47+
2448
export type ReactNetlifyIdentityAPI = {
2549
user: User | undefined;
2650
/** not meant for normal use! you should mostly use one of the other exported methods to update the user instance */
27-
setUser: React.Dispatch<React.SetStateAction<User | undefined>>;
51+
setUser: Dispatch<SetStateAction<User | undefined>>;
2852
isConfirmedUser: boolean;
2953
isLoggedIn: boolean;
3054
signupUser: (
3155
email: string,
3256
password: string,
3357
data: Object
34-
) => Promise<User | undefined>;
58+
) => MaybeUserPromise;
3559
loginUser: (
3660
email: string,
3761
password: string,
3862
remember?: boolean
39-
) => Promise<User | undefined>;
40-
logoutUser: () => Promise<User | undefined>;
63+
) => MaybeUserPromise;
64+
logoutUser: () => MaybeUserPromise;
4165
requestPasswordRecovery: (email: string) => Promise<void>;
42-
recoverAccount: (
43-
token: string,
44-
remember?: boolean | undefined
45-
) => Promise<User>;
46-
updateUser: (fields: { data: object }) => Promise<User | undefined>;
66+
recoverAccount: (remember?: boolean) => MaybeUserPromise;
67+
updateUser: (fields: { data: object }) => MaybeUserPromise;
4768
getFreshJWT: () => Promise<string>;
4869
authedFetch: {
4970
get: (endpoint: string, obj?: {}) => Promise<any>;
@@ -53,14 +74,10 @@ export type ReactNetlifyIdentityAPI = {
5374
};
5475
_goTrueInstance: GoTrue;
5576
_url: string;
56-
loginProvider: (
57-
provider: 'bitbucket' | 'github' | 'gitlab' | 'google'
58-
) => void;
59-
acceptInviteExternalUrl: (
60-
provider: 'bitbucket' | 'github' | 'gitlab' | 'google',
61-
token: string
62-
) => string;
77+
loginProvider: (provider: Provider) => void;
78+
acceptInviteExternalUrl: (provider: Provider) => string;
6379
settings: Settings;
80+
param: TokenParam;
6481
};
6582

6683
const [_useIdentityContext, _IdentityCtxProvider] = createCtx<
@@ -75,7 +92,7 @@ export function IdentityContextProvider({
7592
onAuthChange = () => {},
7693
}: {
7794
url: string;
78-
children: React.ReactNode;
95+
children: ReactNode;
7996
onAuthChange?: authChangeParam;
8097
}) {
8198
/******** SETUP */
@@ -99,7 +116,7 @@ export function useNetlifyIdentity(
99116
onAuthChange: authChangeParam = () => {},
100117
enableRunRoutes: boolean = true
101118
): ReactNetlifyIdentityAPI {
102-
const goTrueInstance = React.useMemo(
119+
const goTrueInstance = useMemo(
103120
() =>
104121
new GoTrue({
105122
APIUrl: `${url}/.netlify/identity`,
@@ -108,87 +125,164 @@ export function useNetlifyIdentity(
108125
[url]
109126
);
110127

111-
const [user, setUser] = React.useState<User | undefined>(
128+
/******* STATE and EFFECTS */
129+
130+
const [user, setUser] = useState<User | undefined>(
112131
goTrueInstance.currentUser() || undefined
113132
);
114-
const _setUser = (_user: User | undefined) => {
115-
setUser(_user);
116-
onAuthChange(_user); // if someone's subscribed to auth changes, let 'em know
117-
return _user; // so that we can continue chaining
118-
};
119133

120-
React.useEffect(() => {
134+
const _setUser = useCallback(
135+
(_user: User | undefined) => {
136+
setUser(_user);
137+
onAuthChange(_user); // if someone's subscribed to auth changes, let 'em know
138+
return _user; // so that we can continue chaining
139+
},
140+
[onAuthChange]
141+
);
142+
143+
const [param, setParam] = useState<TokenParam>(defaultParam);
144+
145+
useEffect(() => {
121146
if (enableRunRoutes) {
122-
runRoutes(goTrueInstance, _setUser);
147+
const param = runRoutes(goTrueInstance, _setUser);
148+
149+
if (param.token || param.error) {
150+
setParam(param);
151+
}
123152
}
124153
}, []);
125154

155+
const [settings, setSettings] = useState<Settings>(defaultSettings);
156+
157+
useEffect(() => {
158+
goTrueInstance.settings
159+
.bind(goTrueInstance)()
160+
.then(x => setSettings(x));
161+
}, []);
162+
126163
/******* OPERATIONS */
127164
// make sure the Registration preferences under Identity settings in your Netlify dashboard are set to Open.
128165
// https://react-netlify-identity.netlify.com/login#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTY0ODY3MjEsInN1YiI6ImNiZjY5MTZlLTNlZGYtNGFkNS1iOTYzLTQ4ZTY2NDcyMDkxNyIsImVtYWlsIjoic2hhd250aGUxQGdtYWlsLmNvbSIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImdpdGh1YiJ9LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMxLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY3NjQ5NTc_dj00IiwiZnVsbF9uYW1lIjoic3d5eCJ9fQ.E8RrnuCcqq-mLi1_Q5WHJ-9THIdQ3ha1mePBKGhudM0&expires_in=3600&refresh_token=OyA_EdRc7WOIVhY7RiRw5w&token_type=bearer
129166
/******* external oauth */
130-
type Provider = 'bitbucket' | 'github' | 'gitlab' | 'google';
131167

132-
const loginProvider = (provider: Provider) => {
133-
const url = goTrueInstance.loginExternalUrl(provider);
134-
window.location.href = url;
135-
};
136-
const acceptInviteExternalUrl = (provider: Provider, token: string) =>
137-
goTrueInstance.acceptInviteExternalUrl(provider, token);
138-
const _settings = goTrueInstance.settings.bind(goTrueInstance);
139-
const [settings, setSettings] = React.useState<Settings>(defaultSettings);
140-
React.useEffect(() => {
141-
_settings().then(x => setSettings(x));
142-
}, []);
168+
const loginProvider = useCallback(
169+
(provider: Provider) => {
170+
const url = goTrueInstance.loginExternalUrl(provider);
171+
window.location.href = url;
172+
},
173+
[goTrueInstance]
174+
);
175+
176+
const acceptInviteExternalUrl = useCallback(
177+
(provider: Provider) => {
178+
if (!param.token || param.type !== 'invite') {
179+
throw new Error(errors.tokenMissingOrInvalid);
180+
}
181+
182+
const url = goTrueInstance.acceptInviteExternalUrl(provider, param.token);
183+
// clean up consumed token
184+
setParam(defaultParam);
185+
186+
return url;
187+
},
188+
[goTrueInstance, param]
189+
);
143190

144191
/******* email auth */
145-
const signupUser = (email: string, password: string, data: Object) =>
146-
goTrueInstance.signup(email, password, data).then(_setUser); // TODO: make setUser optional?
147-
const loginUser = (
148-
email: string,
149-
password: string,
150-
remember: boolean = true
151-
) => goTrueInstance.login(email, password, remember).then(_setUser);
152-
const requestPasswordRecovery = (email: string) =>
153-
goTrueInstance.requestPasswordRecovery(email);
154-
const recoverAccount = (token: string, remember?: boolean | undefined) =>
155-
goTrueInstance.recover(token, remember);
156-
const updateUser = (fields: { data: object }) => {
157-
if (user == null) {
158-
throw new Error('No current user found - are you logged in?');
159-
} else {
192+
const signupUser = useCallback(
193+
(
194+
email: string,
195+
password: string,
196+
data: Object,
197+
directLogin: boolean = true
198+
) =>
199+
goTrueInstance.signup(email, password, data).then(user => {
200+
if (directLogin) {
201+
return _setUser(user);
202+
}
203+
204+
return user;
205+
}),
206+
[goTrueInstance, _setUser]
207+
);
208+
209+
const loginUser = useCallback(
210+
(email: string, password: string, remember: boolean = true) =>
211+
goTrueInstance.login(email, password, remember).then(_setUser),
212+
[goTrueInstance, _setUser]
213+
);
214+
215+
const requestPasswordRecovery = useCallback(
216+
(email: string) => goTrueInstance.requestPasswordRecovery(email),
217+
[goTrueInstance]
218+
);
219+
220+
const recoverAccount = useCallback(
221+
(remember?: boolean | undefined) => {
222+
if (!param.token || param.type !== 'recovery') {
223+
throw new Error(errors.tokenMissingOrInvalid);
224+
}
225+
226+
return goTrueInstance.recover(param.token, remember).then(user => {
227+
// clean up consumed token
228+
setParam(defaultParam);
229+
return _setUser(user);
230+
});
231+
},
232+
[goTrueInstance, _setUser, param]
233+
);
234+
235+
const updateUser = useCallback(
236+
(fields: { data: object }) => {
237+
if (!user) {
238+
throw new Error(errors.noUserFound);
239+
}
240+
160241
return user!
161242
.update(fields) // e.g. { data: { email: "example@example.com", password: "password" } }
162243
.then(_setUser);
244+
},
245+
[user]
246+
);
247+
248+
const getFreshJWT = useCallback(() => {
249+
if (!user) {
250+
throw new Error(errors.noUserFound);
163251
}
164-
};
165-
const getFreshJWT = () => {
166-
if (!user) throw new Error('No current user found - are you logged in?');
252+
167253
return user.jwt();
168-
};
169-
const logoutUser = () => {
170-
if (!user) throw new Error('No current user found - are you logged in?');
254+
}, [user]);
255+
256+
const logoutUser = useCallback(() => {
257+
if (!user) {
258+
throw new Error(errors.noUserFound);
259+
}
260+
171261
return user.logout().then(() => _setUser(undefined));
172-
};
262+
}, [user]);
173263

174264
const genericAuthedFetch = (method: string) => (
175265
endpoint: string,
176-
obj = {}
266+
options: RequestInit = {}
177267
) => {
178-
if (!user || !user.token || !user.token.access_token)
179-
throw new Error('no user token found');
268+
if (!user?.token?.access_token) {
269+
throw new Error(errors.noUserTokenFound);
270+
}
271+
180272
const defaultObj = {
181273
headers: {
182274
Accept: 'application/json',
183275
'Content-Type': 'application/json',
184276
Authorization: 'Bearer ' + user.token.access_token,
185277
},
186278
};
187-
const finalObj = Object.assign(defaultObj, { method }, obj);
279+
const finalObj = Object.assign(defaultObj, { method }, options);
280+
188281
return fetch(endpoint, finalObj).then(res =>
189282
finalObj.headers['Content-Type'] === 'application/json' ? res.json() : res
190283
);
191284
};
285+
192286
const authedFetch = {
193287
get: genericAuthedFetch('GET'),
194288
post: genericAuthedFetch('POST'),
@@ -216,6 +310,7 @@ export function useNetlifyIdentity(
216310
loginProvider,
217311
acceptInviteExternalUrl,
218312
settings,
313+
param,
219314
};
220315
}
221316

@@ -234,9 +329,9 @@ function validateUrl(value: string) {
234329

235330
// lazy initialize contexts without providing a Nullable type upfront
236331
function createCtx<A>() {
237-
const ctx = React.createContext<A | undefined>(undefined);
332+
const ctx = createContext<A | undefined>(undefined);
238333
function useCtx() {
239-
const c = React.useContext(ctx);
334+
const c = useContext(ctx);
240335
if (!c) throw new Error('useCtx must be inside a Provider with a value');
241336
return c;
242337
}

0 commit comments

Comments
 (0)