Skip to content

Commit d4eb2da

Browse files
committed
feat: added telegram contact field, fixed netlify function sig verification
1 parent d1e49c6 commit d4eb2da

File tree

6 files changed

+136
-115
lines changed

6 files changed

+136
-115
lines changed

web/netlify/functions/update-settings.ts

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,44 @@
1-
import { createPublicClient, http } from "viem";
1+
import { Handler } from "@netlify/functions";
2+
import { verifyMessage } from "viem";
23
import { createClient } from "@supabase/supabase-js";
34
import { arbitrumGoerli } from "viem/chains";
45

5-
const publicClient = createPublicClient({
6-
chain: arbitrumGoerli,
7-
transport: http(),
8-
});
9-
106
const SUPABASE_KEY = process.env.SUPABASE_CLIENT_API_KEY;
117
const SUPABASE_URL = process.env.SUPABASE_URL;
128
const supabase = createClient(SUPABASE_URL!, SUPABASE_KEY!);
139

14-
export const handler = async function (event: any, context: any) {
10+
export const handler: Handler = async (event) => {
1511
try {
16-
const { message, address, signature } = JSON.parse(event.body);
17-
const email = message.split("Email:").pop().split(",Nonce:")[0].trim();
18-
const nonce = message.split("Nonce:").pop().trim();
19-
const isValid = await publicClient.verifyMessage({ address, message: message, signature });
20-
// If the recovered address does not match the provided address, return an error
12+
if (!event.body) {
13+
throw new Error("No body provided");
14+
}
15+
// TODO: sanitize the body
16+
const { email, telegram, nonce, address, signature } = JSON.parse(event.body);
17+
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
18+
const data = {
19+
address: lowerCaseAddress,
20+
message: `Email:${email},Nonce:${nonce}`,
21+
signature,
22+
};
23+
// Note: this does NOT work for smart contract wallets, but viem's publicClient.verifyMessage() fails to verify atm.
24+
// https://viem.sh/docs/utilities/verifyMessage.html
25+
const isValid = await verifyMessage(data);
2126
if (!isValid) {
27+
// If the recovered address does not match the provided address, return an error
2228
throw new Error("Signature verification failed");
2329
}
24-
// Allowed columns to update
25-
const allowedColumns = ["discord", "telegram", "twitter", "matrix", "push", "email"];
30+
// TODO: use typed supabase client
2631
// If the message is empty, delete the user record
27-
if (email === "") {
28-
const { data, error } = await supabase.from("users").delete().match({ address: address });
32+
if (email === "" && telegram === "") {
33+
const { error } = await supabase.from("users").delete().match({ address: lowerCaseAddress });
2934
if (error) throw error;
3035
return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) };
3136
}
32-
// Parse the signed message console.log("2", email);
33-
const parsedMessage = JSON.parse(JSON.stringify(email));
34-
// Prepare the record data based on the allowed columns
35-
const recordData: { [key: string]: any } = {};
36-
for (const key in parsedMessage) {
37-
if (allowedColumns.includes(key)) {
38-
recordData[key] = parsedMessage[key];
39-
}
40-
}
41-
// Assuming you have a 'users' table with 'address' and allowedColumns fields
42-
const { data, error } = await supabase
37+
// For a user matching this address, upsert the user record
38+
const { error } = await supabase
4339
.from("user-settings")
44-
.upsert({ address, email: email })
45-
.match({ address: address });
40+
.upsert({ address: lowerCaseAddress, email: email, telegram: telegram })
41+
.match({ address: lowerCaseAddress });
4642
if (error) throw error;
4743
return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) };
4844
} catch (err) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { Dispatch, SetStateAction, useMemo, useEffect } from "react";
2+
import styled from "styled-components";
3+
4+
import { Field } from "@kleros/ui-components-library";
5+
6+
const StyledLabel = styled.label`
7+
display: flex;
8+
justify-content: space-between;
9+
margin-bottom: 10px;
10+
`;
11+
12+
const StyledField = styled(Field)`
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
width: 100%;
17+
// TODO: make the placeholder text color lighter or ~80% opaque
18+
`;
19+
20+
interface IForm {
21+
contactLabel: string;
22+
contactPlaceholder: string;
23+
contactInput: string;
24+
contactIsValid: boolean;
25+
setContactInput: Dispatch<SetStateAction<string>>;
26+
setContactIsValid: Dispatch<SetStateAction<boolean>>;
27+
validator: RegExp;
28+
}
29+
30+
const FormContact: React.FC<IForm> = ({
31+
contactLabel,
32+
contactPlaceholder,
33+
contactInput,
34+
contactIsValid,
35+
setContactInput,
36+
setContactIsValid,
37+
validator,
38+
}) => {
39+
useEffect(() => {
40+
setContactIsValid(validator.test(contactInput));
41+
}, [contactInput, setContactIsValid, validator]);
42+
43+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
44+
event.preventDefault();
45+
setContactInput(event.target.value);
46+
};
47+
48+
const fieldVariant = useMemo(() => {
49+
if (contactInput === "") {
50+
return undefined;
51+
}
52+
return contactIsValid ? "success" : "error";
53+
}, [contactInput, contactIsValid]);
54+
55+
return (
56+
<>
57+
<StyledLabel>{contactLabel}</StyledLabel>
58+
<StyledField
59+
variant={fieldVariant}
60+
value={contactInput}
61+
onChange={handleInputChange}
62+
placeholder={contactPlaceholder}
63+
/>
64+
</>
65+
);
66+
};
67+
68+
export default FormContact;

web/src/layout/Header/navbar/Menu/Settings/SendMeNotifications/FormNotifs/FormEmail.tsx

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

web/src/layout/Header/navbar/Menu/Settings/SendMeNotifications/FormNotifs/index.tsx

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useState } from "react";
22
import styled from "styled-components";
33
import { useWalletClient, useAccount } from "wagmi";
4-
import { Checkbox, Button } from "@kleros/ui-components-library";
4+
import { Button } from "@kleros/ui-components-library";
55
import { uploadSettingsToSupabase } from "utils/uploadSettingsToSupabase";
6-
import FormEmail from "./FormEmail";
6+
import FormContact from "./FormContact";
77

88
const FormContainer = styled.form`
99
position: relative;
@@ -13,70 +13,71 @@ const FormContainer = styled.form`
1313
padding-bottom: 16px;
1414
`;
1515

16-
const StyledCheckbox = styled(Checkbox)`
17-
margin-top: 20px;
18-
`;
19-
2016
const ButtonContainer = styled.div`
2117
display: flex;
2218
justify-content: end;
23-
margin-top: 16px;
2419
`;
2520

26-
const FormEmailContainer = styled.div`
27-
position: relative;
21+
const FormContactContainer = styled.div`
22+
display: flex;
23+
flex-direction: column;
24+
margin-bottom: 24px;
2825
`;
2926

30-
const OPTIONS = [{ label: "When x." }, { label: "When y." }, { label: "When z." }, { label: "When w." }];
31-
3227
const FormNotifs: React.FC = () => {
33-
const [checkboxStates, setCheckboxStates] = useState<boolean[]>(new Array(OPTIONS.length).fill(false));
28+
const [telegramInput, setTelegramInput] = useState<string>("");
3429
const [emailInput, setEmailInput] = useState<string>("");
30+
const [telegramIsValid, setTelegramIsValid] = useState<boolean>(false);
3531
const [emailIsValid, setEmailIsValid] = useState<boolean>(false);
3632
const { data: walletClient } = useWalletClient();
3733
const { address } = useAccount();
3834

39-
const handleCheckboxChange = (index: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
40-
const newCheckboxStates = [...checkboxStates];
41-
newCheckboxStates[index] = e.target.checked;
42-
setCheckboxStates(newCheckboxStates);
43-
};
35+
// TODO: retrieve the current email address from the database and populate the email input with it
4436

4537
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
4638
e.preventDefault();
47-
const nonce = new Date().getTime();
48-
const message = `Email:${emailInput},Nonce:${nonce}`;
39+
const nonce = new Date().getTime().toString();
4940
const signature = await walletClient?.signMessage({
5041
account: address,
51-
message: message,
42+
message: `Email:${emailInput},Nonce:${nonce}`,
5243
});
44+
if (!address || !signature) {
45+
console.error("Missing address or signature");
46+
return;
47+
}
5348
const data = {
54-
message: message,
49+
email: emailInput,
50+
telegram: telegramInput,
51+
nonce,
5552
address,
56-
signature: signature,
53+
signature,
5754
};
58-
5955
await uploadSettingsToSupabase(data);
6056
};
6157
return (
6258
<FormContainer onSubmit={handleSubmit}>
63-
{OPTIONS.map(({ label }, index) => (
64-
<StyledCheckbox
65-
key={label}
66-
onChange={handleCheckboxChange(index)}
67-
checked={checkboxStates[index]}
68-
small={true}
69-
label={label}
59+
<FormContactContainer>
60+
<FormContact
61+
contactLabel="Telegram"
62+
contactPlaceholder="@my_handle"
63+
contactInput={telegramInput}
64+
contactIsValid={telegramIsValid}
65+
setContactInput={setTelegramInput}
66+
setContactIsValid={setTelegramIsValid}
67+
validator={/^@[a-zA-Z0-9_]{5,32}$/}
7068
/>
71-
))}
72-
<FormEmailContainer>
73-
<FormEmail
74-
emailInput={emailInput}
75-
emailIsValid={emailIsValid}
76-
setEmailInput={setEmailInput}
77-
setEmailIsValid={setEmailIsValid}
69+
</FormContactContainer>
70+
<FormContactContainer>
71+
<FormContact
72+
contactLabel="Email"
73+
contactPlaceholder="your.email@email.com"
74+
contactInput={emailInput}
75+
contactIsValid={emailIsValid}
76+
setContactInput={setEmailInput}
77+
setContactIsValid={setEmailIsValid}
78+
validator={/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/}
7879
/>
79-
</FormEmailContainer>
80+
</FormContactContainer>
8081

8182
<ButtonContainer>
8283
<Button text="Save" disabled={!emailIsValid} />

web/src/layout/Header/navbar/Menu/Settings/SendMeNotifications/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const HeaderContainer = styled.div`
2222
`;
2323

2424
const HeaderNotifs: React.FC = () => {
25-
return <HeaderContainer>Send Me Notifications</HeaderContainer>;
25+
return <HeaderContainer>Contact Details</HeaderContainer>;
2626
};
2727

2828
const EnsureChainContainer = styled.div`

web/src/utils/uploadSettingsToSupabase.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { toast } from "react-toastify";
22
import { OPTIONS } from "utils/wrapWithToast";
33

44
type SettingsToSupabaseData = {
5-
message: string;
5+
email: string;
6+
telegram: string;
7+
nonce: string;
68
address: `0x${string}`;
79
signature: `0x${string}`;
810
};

0 commit comments

Comments
 (0)