Skip to content

feat(web): notification-system #1210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6f9a5db
feat(web): notification-system
nhestrompia Aug 31, 2023
0927267
refactor: netlify function
nhestrompia Aug 31, 2023
653793e
fix(web): make sure you are connected before settings notifications c…
kemuru Sep 2, 2023
3650b3d
feat(web): add nonce to message
kemuru Sep 7, 2023
ee63a36
chore(web): yarn lock problem
kemuru Sep 7, 2023
7cc3dd4
fix(web): change update settings to viem
kemuru Sep 7, 2023
bcbf73b
fix(web): payload json stringified
kemuru Sep 7, 2023
2b6cfe4
fix(web): wrong netlify folder path
kemuru Sep 7, 2023
ef94986
chore(web): remove new lines on message
kemuru Sep 8, 2023
bdc9403
chore(web): remove logs and add settings type
kemuru Sep 8, 2023
b799786
refactor(web): account abstraction for verifymessage
kemuru Sep 8, 2023
b0ee92d
refactor(web): function name change
kemuru Sep 8, 2023
d1e49c6
fix: added error message
jaybuidl Oct 3, 2023
d4eb2da
feat: added telegram contact field, fixed netlify function sig verifi…
jaybuidl Oct 3, 2023
889cef7
feat: use eip712 typed structured data signing
jaybuidl Oct 3, 2023
2126efc
refactor: notification form filenames
jaybuidl Oct 3, 2023
699e699
feat: close the settings popup if saved successfully
jaybuidl Oct 3, 2023
f72f819
chore: added types generation for the supabase db client
jaybuidl Oct 4, 2023
1365a27
feat: hardened input validation
jaybuidl Oct 6, 2023
657bb22
fix: user message
jaybuidl Oct 9, 2023
d1bd9d1
Merge branch 'dev' into feat(web)/notification-system
jaybuidl Oct 9, 2023
967e78f
fix: interface changes after merge
jaybuidl Oct 9, 2023
788a48b
chore: upgraded to the latest ui-components which decreases the opaci…
jaybuidl Oct 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions web/netlify/functions/update-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Handler } from "@netlify/functions";
import { verifyTypedData } from "viem";
import { createClient } from "@supabase/supabase-js";
import messages from "../../src/consts/eip712-messages";

const SUPABASE_KEY = process.env.SUPABASE_CLIENT_API_KEY;
const SUPABASE_URL = process.env.SUPABASE_URL;
const supabase = createClient(SUPABASE_URL!, SUPABASE_KEY!);

export const handler: Handler = async (event) => {
try {
if (!event.body) {
throw new Error("No body provided");
}
// TODO: sanitize event.body
const { email, telegram, nonce, address, signature } = JSON.parse(event.body);
const lowerCaseAddress = address.toLowerCase() as `0x${string}`;
// Note: this does NOT work for smart contract wallets, but viem's publicClient.verifyMessage() fails to verify atm.
// https://viem.sh/docs/utilities/verifyTypedData.html
const isValid = await verifyTypedData({
...messages.contactDetails(address, nonce, telegram, email),
signature,
});
if (!isValid) {
// If the recovered address does not match the provided address, return an error
throw new Error("Signature verification failed");
}
// TODO: use typed supabase client
// If the message is empty, delete the user record
if (email === "" && telegram === "") {
const { error } = await supabase.from("users").delete().match({ address: lowerCaseAddress });
if (error) throw error;
return { statusCode: 200, body: JSON.stringify({ message: "Record deleted successfully." }) };
}
// For a user matching this address, upsert the user record
const { error } = await supabase
.from("user-settings")
.upsert({ address: lowerCaseAddress, email: email, telegram: telegram })
.match({ address: lowerCaseAddress });
if (error) throw error;
return { statusCode: 200, body: JSON.stringify({ message: "Record updated successfully." }) };
} catch (err) {
return { statusCode: 500, body: JSON.stringify({ message: `Error: ${err}` }) };
}
};
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@kleros/ui-components-library": "^2.6.1",
"@sentry/react": "^7.55.2",
"@sentry/tracing": "^7.55.2",
"@supabase/supabase-js": "^2.33.1",
"@tanstack/react-query": "^4.28.0",
"@types/react-modal": "^3.16.0",
"@web3modal/ethereum": "^2.7.1",
Expand Down
24 changes: 24 additions & 0 deletions web/src/consts/eip712-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default {
contactDetails: (address: `0x${string}`, nonce, telegram = "", email = "") =>
({
address: address.toLowerCase() as `0x${string}`,
domain: {
name: "Kleros v2",
version: "1",
chainId: 421_613,
},
types: {
ContactDetails: [
{ name: "email", type: "string" },
{ name: "telegram", type: "string" },
{ name: "nonce", type: "string" },
],
},
primaryType: "ContactDetails",
message: {
email,
telegram,
nonce,
},
} as const),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { Dispatch, SetStateAction, useMemo, useEffect } from "react";
import styled from "styled-components";

import { Field } from "@kleros/ui-components-library";

const StyledLabel = styled.label`
display: flex;
justify-content: space-between;
margin-bottom: 10px;
`;

const StyledField = styled(Field)`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
// TODO: make the placeholder text color lighter or ~80% opaque
`;

interface IForm {
contactLabel: string;
contactPlaceholder: string;
contactInput: string;
contactIsValid: boolean;
setContactInput: Dispatch<SetStateAction<string>>;
setContactIsValid: Dispatch<SetStateAction<boolean>>;
validator: RegExp;
}

const FormContact: React.FC<IForm> = ({
contactLabel,
contactPlaceholder,
contactInput,
contactIsValid,
setContactInput,
setContactIsValid,
validator,
}) => {
useEffect(() => {
setContactIsValid(validator.test(contactInput));
}, [contactInput, setContactIsValid, validator]);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
setContactInput(event.target.value);
};

const fieldVariant = useMemo(() => {
if (contactInput === "") {
return undefined;
}
return contactIsValid ? "success" : "error";
}, [contactInput, contactIsValid]);

return (
<>
<StyledLabel>{contactLabel}</StyledLabel>
<StyledField
variant={fieldVariant}
value={contactInput}
onChange={handleInputChange}
placeholder={contactPlaceholder}
/>
</>
);
};

export default FormContact;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState } from "react";
import styled from "styled-components";
import { useWalletClient, useAccount } from "wagmi";
import { Button } from "@kleros/ui-components-library";
import { uploadSettingsToSupabase } from "utils/uploadSettingsToSupabase";
import FormContact from "./FormContact";
import messages from "../../../../../../../consts/eip712-messages";
import { ISettings } from "../../types";

const FormContainer = styled.form`
position: relative;
display: flex;
flex-direction: column;
padding: 0 calc(12px + (32 - 12) * ((100vw - 300px) / (1250 - 300)));
padding-bottom: 16px;
`;

const ButtonContainer = styled.div`
display: flex;
justify-content: end;
`;

const FormContactContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 24px;
`;

const FormContactDetails: React.FC<ISettings> = ({ setIsSettingsOpen }) => {
const [telegramInput, setTelegramInput] = useState<string>("");
const [emailInput, setEmailInput] = useState<string>("");
const [telegramIsValid, setTelegramIsValid] = useState<boolean>(false);
const [emailIsValid, setEmailIsValid] = useState<boolean>(false);
const { data: walletClient } = useWalletClient();
const { address } = useAccount();

// TODO: after the user is authenticated, retrieve the current email/telegram from the database and populate the form

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!address) {
throw new Error("Missing address");
}
const nonce = new Date().getTime().toString();
const signature = await walletClient?.signTypedData(
messages.contactDetails(address, nonce, telegramInput, emailInput)
);
if (!signature) {
throw new Error("Missing signature");
}
const data = {
email: emailInput,
telegram: telegramInput,
nonce,
address,
signature,
};
const response = await uploadSettingsToSupabase(data);
if (response.ok) {
setIsSettingsOpen(false);
}
};
return (
<FormContainer onSubmit={handleSubmit}>
<FormContactContainer>
<FormContact
contactLabel="Telegram"
contactPlaceholder="@my_handle"
contactInput={telegramInput}
contactIsValid={telegramIsValid}
setContactInput={setTelegramInput}
setContactIsValid={setTelegramIsValid}
validator={/^@[a-zA-Z0-9_]{5,32}$/}
/>
</FormContactContainer>
<FormContactContainer>
<FormContact
contactLabel="Email"
contactPlaceholder="[email protected]"
contactInput={emailInput}
contactIsValid={emailIsValid}
setContactInput={setEmailInput}
setContactIsValid={setEmailIsValid}
validator={/^([a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/}
/>
</FormContactContainer>

<ButtonContainer>
<Button text="Save" disabled={!emailIsValid && !telegramIsValid} />
</ButtonContainer>
</FormContainer>
);
};

export default FormContactDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import styled from "styled-components";
import { ISettings } from "../types";

import FormContactDetails from "./FormContactDetails";
import { EnsureChain } from "components/EnsureChain";

const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
`;

const HeaderContainer = styled.div`
display: flex;
justify-content: center;
font-size: 16px;
font-weight: 600;
color: ${({ theme }) => theme.primaryText};
margin-top: 16px;
margin-bottom: 12px;
`;

const HeaderNotifs: React.FC = () => {
return <HeaderContainer>Contact Details</HeaderContainer>;
};

const EnsureChainContainer = styled.div`
display: flex;
justify-content: center;
padding-top: 16px;
padding-bottom: 16px;
`;

const NotificationSettings: React.FC<ISettings> = ({ setIsSettingsOpen }) => {
return (
<EnsureChainContainer>
<EnsureChain>
<Container>
<HeaderNotifs />
<FormContactDetails setIsSettingsOpen={setIsSettingsOpen} />
</Container>
</EnsureChain>
</EnsureChainContainer>
);
};

export default NotificationSettings;

This file was deleted.

Loading