Skip to content

Commit cc3a8be

Browse files
committed
feat(web): user-settings-integration
1 parent cff6ee0 commit cc3a8be

File tree

8 files changed

+318
-41
lines changed

8 files changed

+318
-41
lines changed

web/src/consts/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export const RELEASE_VERSION = version;
2121

2222
// https://www.w3.org/TR/2012/WD-html-markup-20120329/input.email.html#input.email.attrs.value.single
2323
// eslint-disable-next-line security/detect-unsafe-regex
24-
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
24+
export const EMAIL_REGEX =
25+
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
2526
export const TELEGRAM_REGEX = /^@\w{5,32}$/;
2627
export const ETH_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
2728
export const ETH_SIGNATURE_REGEX = /^0x[a-fA-F0-9]{130}$/;

web/src/context/AtlasProvider.tsx

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
import React, { useMemo, createContext, useContext, useState, useCallback, useEffect } from "react";
22

3+
import { useQuery } from "@tanstack/react-query";
34
import { GraphQLClient } from "graphql-request";
45
import { decodeJwt } from "jose";
56
import { useAccount, useChainId, useSignMessage } from "wagmi";
67

78
import { useSessionStorage } from "hooks/useSessionStorage";
8-
import { createMessage, getNonce, loginUser } from "utils/atlas";
9+
import {
10+
createMessage,
11+
getNonce,
12+
loginUser,
13+
addUser as addUserToAtlas,
14+
fetchUser,
15+
updateUser as updateUserInAtlas,
16+
type User,
17+
type AddUserData,
18+
type UpdateUserData,
19+
} from "utils/atlas";
20+
21+
import { isUndefined } from "src/utils";
922

1023
interface IAtlasProvider {
1124
isVerified: boolean;
1225
isSigningIn: boolean;
13-
26+
isAddingUser: boolean;
27+
isFetchingUser: boolean;
28+
isUpdatingUser: boolean;
29+
user: User | undefined;
30+
userExists: boolean;
1431
authoriseUser: () => void;
32+
addUser: (userSettings: AddUserData) => Promise<boolean>;
33+
updateUser: (userSettings: UpdateUserData) => Promise<boolean>;
1534
}
1635

1736
const Context = createContext<IAtlasProvider | undefined>(undefined);
@@ -26,13 +45,15 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
2645
const chainId = useChainId();
2746
const [authToken, setAuthToken] = useSessionStorage<string | undefined>("authToken", undefined);
2847
const [isSigningIn, setIsSigningIn] = useState(false);
48+
const [isAddingUser, setIsAddingUser] = useState(false);
49+
const [isUpdatingUser, setIsUpdatingUser] = useState(false);
2950
const [isVerified, setIsVerified] = useState(false);
3051
const { signMessageAsync } = useSignMessage();
3152

3253
const atlasGqlClient = useMemo(() => {
3354
const headers = authToken
3455
? {
35-
authorization: authToken,
56+
authorization: `Bearer ${authToken}`,
3657
}
3758
: undefined;
3859
return new GraphQLClient(atlasUri, { headers });
@@ -71,6 +92,30 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
7192
};
7293
}, [authToken, verifySession]);
7394

95+
const {
96+
data: user,
97+
isLoading: isFetchingUser,
98+
refetch: refetchUser,
99+
} = useQuery({
100+
queryKey: [`UserSettings`],
101+
enabled: isVerified && !isUndefined(address),
102+
staleTime: Infinity,
103+
queryFn: async () => {
104+
try {
105+
if (!isVerified || isUndefined(address)) return undefined;
106+
return await fetchUser(atlasGqlClient, address);
107+
} catch {
108+
return undefined;
109+
}
110+
},
111+
});
112+
113+
// this would change based on the fields we have and what defines a user to be existing
114+
const userExists = useMemo(() => {
115+
if (!user) return false;
116+
return user.email ? true : false;
117+
}, [user]);
118+
74119
/**
75120
* @description authorise user and enable authorised calls
76121
*/
@@ -94,9 +139,86 @@ const AtlasProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) =
94139
}
95140
}, [address, chainId, setAuthToken, signMessageAsync, atlasGqlClient]);
96141

142+
/**
143+
* @description adds a new user to atlas
144+
* @param {AddUserData} userSettings - object containing data to be added
145+
* @returns {Promise<boolean>} A promise that resolves to true if the user was added successfully
146+
*/
147+
const addUser = useCallback(
148+
async (userSettings: AddUserData) => {
149+
try {
150+
if (!address || !isVerified) return false;
151+
setIsAddingUser(true);
152+
153+
const userAdded = await addUserToAtlas(atlasGqlClient, { address, ...userSettings });
154+
refetchUser();
155+
156+
return userAdded;
157+
} catch (err: any) {
158+
// eslint-disable-next-line
159+
console.log("Add User Error : ", err?.message);
160+
return false;
161+
} finally {
162+
setIsAddingUser(false);
163+
}
164+
},
165+
[address, isVerified, setIsAddingUser, atlasGqlClient, refetchUser]
166+
);
167+
168+
/**
169+
* @description updates user settings in atlas
170+
* @param {UpdateUserData} userSettings - object containing data to be updated
171+
* @returns {Promise<boolean>} A promise that resolves to true if settings were updated successfully
172+
*/
173+
const updateUser = useCallback(
174+
async (userSettings: UpdateUserData) => {
175+
try {
176+
if (!address || !isVerified) return false;
177+
setIsUpdatingUser(true);
178+
179+
const userUpdated = await updateUserInAtlas(atlasGqlClient, userSettings);
180+
refetchUser();
181+
182+
return userUpdated;
183+
} catch (err: any) {
184+
// eslint-disable-next-line
185+
console.log("Update User Error : ", err?.message);
186+
return false;
187+
} finally {
188+
setIsUpdatingUser(false);
189+
}
190+
},
191+
[address, isVerified, setIsUpdatingUser, atlasGqlClient, refetchUser]
192+
);
193+
97194
return (
98195
<Context.Provider
99-
value={useMemo(() => ({ isVerified, isSigningIn, authoriseUser }), [isVerified, isSigningIn, authoriseUser])}
196+
value={useMemo(
197+
() => ({
198+
isVerified,
199+
isSigningIn,
200+
isAddingUser,
201+
authoriseUser,
202+
addUser,
203+
user,
204+
isFetchingUser,
205+
updateUser,
206+
isUpdatingUser,
207+
userExists,
208+
}),
209+
[
210+
isVerified,
211+
isSigningIn,
212+
isAddingUser,
213+
authoriseUser,
214+
addUser,
215+
user,
216+
isFetchingUser,
217+
updateUser,
218+
isUpdatingUser,
219+
userExists,
220+
]
221+
)}
100222
>
101223
{children}
102224
</Context.Provider>

web/src/hooks/useSessionStorage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useState } from "react";
33
export function useSessionStorage<T>(keyName: string, defaultValue: T) {
44
const [storedValue, setStoredValue] = useState<T>(() => {
55
try {
6-
const value = window.localStorage.getItem(keyName);
6+
const value = window.sessionStorage.getItem(keyName);
7+
78
return value ? JSON.parse(value) : defaultValue;
89
} catch (err) {
910
return defaultValue;

web/src/layout/Header/navbar/Menu/Settings/Notifications/FormContactDetails/index.tsx

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import React, { useEffect, useMemo, useState } from "react";
22
import styled from "styled-components";
3+
34
import { useAccount } from "wagmi";
5+
46
import { Button } from "@kleros/ui-components-library";
5-
import { uploadSettingsToSupabase } from "utils/uploadSettingsToSupabase";
6-
import FormContact from "./FormContact";
7-
import { EMAIL_REGEX, TELEGRAM_REGEX } from "consts/index";
7+
8+
import { EMAIL_REGEX } from "consts/index";
9+
import { useAtlasProvider } from "context/AtlasProvider";
810

911
import { responsiveSize } from "styles/responsiveSize";
1012

1113
import { ISettings } from "../../../../index";
12-
import { useUserSettings } from "hooks/queries/useUserSettings";
14+
15+
import FormContact from "./FormContact";
1316

1417
const FormContainer = styled.form`
1518
width: 100%;
@@ -32,33 +35,21 @@ const FormContactContainer = styled.div`
3235
`;
3336

3437
const FormContactDetails: React.FC<ISettings> = ({ toggleIsSettingsOpen }) => {
35-
const [telegramInput, setTelegramInput] = useState<string>("");
3638
const [emailInput, setEmailInput] = useState<string>("");
37-
const [telegramIsValid, setTelegramIsValid] = useState<boolean>(false);
3839
const [emailIsValid, setEmailIsValid] = useState<boolean>(false);
3940
const { address } = useAccount();
40-
const { data: userSettings, refetch: refetchUserSettings } = useUserSettings();
41+
const { user, isAddingUser, isFetchingUser, addUser, updateUser, isUpdatingUser, userExists } = useAtlasProvider();
4142

4243
const isEditingEmail = useMemo(() => {
43-
if (!userSettings?.email && emailInput === "") return false;
44-
return userSettings?.email !== emailInput;
45-
}, [userSettings, emailInput]);
46-
47-
const isEditingTelegram = useMemo(() => {
48-
if (!userSettings?.telegram && telegramInput === "") return false;
49-
return userSettings?.telegram !== telegramInput;
50-
}, [userSettings, telegramInput]);
51-
52-
useEffect(() => {
53-
refetchUserSettings();
54-
}, [address]);
44+
if (!user?.email && emailInput === "") return false;
45+
return user?.email !== emailInput;
46+
}, [user, emailInput]);
5547

5648
useEffect(() => {
57-
if (!userSettings) return;
49+
if (!user) return;
5850

59-
setEmailInput(userSettings.email ?? "");
60-
setTelegramInput(userSettings.telegram ?? "");
61-
}, [userSettings]);
51+
setEmailInput(user.email);
52+
}, [user]);
6253

6354
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
6455
e.preventDefault();
@@ -68,18 +59,26 @@ const FormContactDetails: React.FC<ISettings> = ({ toggleIsSettingsOpen }) => {
6859

6960
const data = {
7061
email: emailInput,
71-
telegram: telegramInput,
72-
address,
7362
};
7463

75-
uploadSettingsToSupabase(data)
76-
.then(async (res) => {
77-
if (res.ok) {
78-
toggleIsSettingsOpen();
79-
refetchUserSettings();
80-
}
81-
})
82-
.catch((err) => console.log(err));
64+
// if user exists then update
65+
if (userExists) {
66+
updateUser(data)
67+
.then(async (res) => {
68+
if (res) {
69+
toggleIsSettingsOpen();
70+
}
71+
})
72+
.catch((err) => console.log(err));
73+
} else {
74+
addUser(data)
75+
.then(async (res) => {
76+
if (res) {
77+
toggleIsSettingsOpen();
78+
}
79+
})
80+
.catch((err) => console.log(err));
81+
}
8382
};
8483
return (
8584
<FormContainer onSubmit={handleSubmit}>
@@ -109,8 +108,10 @@ const FormContactDetails: React.FC<ISettings> = ({ toggleIsSettingsOpen }) => {
109108
</FormContactContainer>
110109

111110
<ButtonContainer>
112-
{/* <Button text="Save" disabled={(!isEditingEmail && !isEditingTelegram) || !emailIsValid || !telegramIsValid} /> */}
113-
<Button text="Save" disabled={!isEditingEmail || !emailIsValid} />
111+
<Button
112+
text="Save"
113+
disabled={!isEditingEmail || !emailIsValid || isAddingUser || isFetchingUser || isUpdatingUser}
114+
/>
114115
</ButtonContainer>
115116
</FormContainer>
116117
);

web/src/utils/atlas/addUser.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { gql, type GraphQLClient } from "graphql-request";
2+
import { toast } from "react-toastify";
3+
import { Address } from "viem";
4+
5+
import { OPTIONS } from "utils/wrapWithToast";
6+
7+
const query = gql`
8+
mutation AddUser($settings: AddUserSettingsDto!) {
9+
addUser(addUserSettings: $settings)
10+
}
11+
`;
12+
13+
export type AddUserData = {
14+
address?: Address;
15+
email: string;
16+
};
17+
18+
type AddUserResponse = {
19+
addUser: boolean;
20+
};
21+
22+
export function addUser(client: GraphQLClient, userData: AddUserData): Promise<boolean> {
23+
const variables = {
24+
settings: userData,
25+
};
26+
27+
return toast.promise<boolean, Error>(
28+
client
29+
.request<AddUserResponse>(query, variables)
30+
.then(async (response) => response.addUser)
31+
.catch((errors) => {
32+
// eslint-disable-next-line no-console
33+
console.log("Add User error:", { errors });
34+
35+
const errorMessage = Array.isArray(errors?.response?.errors)
36+
? errors.response.errors[0]?.message
37+
: "Unknown error";
38+
throw new Error(errorMessage);
39+
}),
40+
{
41+
pending: `Adding User ...`,
42+
success: "User added successfully!",
43+
error: {
44+
render({ data: error }) {
45+
return `Adding User failed: ${error?.message}`;
46+
},
47+
},
48+
},
49+
OPTIONS
50+
);
51+
}

0 commit comments

Comments
 (0)