From fccd6f61da42a5d76b13b5992f73c4d13aaf1ad1 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:37:20 +0200 Subject: [PATCH 01/13] feat(SPV-679): upsert and serach contacts with actual backend functionality --- src/api/requests/contact.ts | 45 +++++++++++-------- src/api/types/contact.ts | 13 ++++-- .../ContactsTable.tsx/ContactsTable.tsx | 6 +-- .../ContactsTable.tsx/JustAddedContcatMsg.tsx | 2 +- .../ContactsTable.tsx/StatusBadge.tsx | 11 ++++- .../ContactAddModal/ContactAddModal.tsx | 5 ++- .../_modals/VerifyModal/VerifyModal.tsx | 10 ++--- 7 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/api/requests/contact.ts b/src/api/requests/contact.ts index a96c700..5849305 100644 --- a/src/api/requests/contact.ts +++ b/src/api/requests/contact.ts @@ -2,22 +2,17 @@ import { timeoutPromise } from '@/utils/timeoutPromise'; import { PaginationParams } from '../types'; import { Contact } from '../types/contact'; +import axios from 'axios'; export const searchContacts = async (_pagination?: PaginationParams) => { - await timeoutPromise(500); - return contacts; + const { data: response } = await axios.get(`/contact/search`); + return response; }; -export const addContact = async (paymail: string, name: string) => { - await timeoutPromise(1000); - contacts = [ - ...contacts, - { - paymail, - name, - status: 'awaiting-acceptance', - }, - ]; +export const upsertContact = async (paymail: string, fullName: string) => { + await axios.put(`/contact/${encodeURIPaymail(paymail)}`, { + fullName, + }); }; export const rejectContact = async (paymail: string) => { @@ -30,7 +25,7 @@ export const acceptContact = async (paymail: string) => { await timeoutPromise(1000); contacts = contacts.map((contact) => { if (contact.paymail === paymail) { - contact.status = 'not-confirmed'; + contact.status = 'unconfirmed'; } return { ...contact }; }); @@ -51,22 +46,36 @@ export const confirmContactWithTOTP = async (paymail: string, _totp: number) => }); }; +const encodeURIPaymail = (paymail: string) => { + // Remove control characters from the paymail + function* iterator() { + for (let i = 0; i < paymail.length; i++) { + const code = paymail.charCodeAt(i); + if (code > 32 && code !== 127) { + yield code; + } + } + } + const sanitizedPaymail = String.fromCharCode(...iterator()); + return encodeURIComponent(sanitizedPaymail); +}; + /// Mocked contacts let contacts: Contact[] = [ { paymail: 'bob@example.com', - name: 'Bob', + fullName: 'Bob', status: 'confirmed', }, { paymail: 'tester@example.com', - name: 'Tester', - status: 'awaiting-acceptance', + fullName: 'Tester', + status: 'awaiting', }, { paymail: 'theguy@example.com', - name: 'The Guy', - status: 'not-confirmed', + fullName: 'The Guy', + status: 'unconfirmed', }, ]; diff --git a/src/api/types/contact.ts b/src/api/types/contact.ts index bd202ad..047397a 100644 --- a/src/api/types/contact.ts +++ b/src/api/types/contact.ts @@ -1,12 +1,17 @@ -export const ContactAwaitingAcceptance = 'awaiting-acceptance'; -export const ContactNotConfirmed = 'not-confirmed'; +export const ContactAwaitingAcceptance = 'awaiting'; +export const ContactNotConfirmed = 'unconfirmed'; export const ContactConfirmed = 'confirmed'; +export const ContactRejected = 'rejected'; -export type ContactStatus = typeof ContactAwaitingAcceptance | typeof ContactNotConfirmed | typeof ContactConfirmed; +export type ContactStatus = + | typeof ContactAwaitingAcceptance + | typeof ContactNotConfirmed + | typeof ContactConfirmed + | typeof ContactRejected; export type Contact = { paymail: string; - name: string; + fullName: string; status: ContactStatus; //TODO: Add more fields (...metadata) diff --git a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx index ddfe9eb..c9c902d 100644 --- a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx @@ -57,10 +57,10 @@ export const ContactsTable: FC = () => { - {sortedContacts.map(({ paymail, status, name }) => ( + {sortedContacts.map(({ paymail, status, fullName }) => ( {paymail} - {name} + {fullName} @@ -101,7 +101,7 @@ export const ContactsTable: FC = () => { setJustAddedContact(false); }} > - {justAddedContact && contactForVerification.status === 'not-confirmed' && } + {justAddedContact && contactForVerification.status === 'unconfirmed' && } )} diff --git a/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx b/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx index 7bb3bd1..f59165d 100644 --- a/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx @@ -7,7 +7,7 @@ export const JustAddedContactMsg: FC = () => { return ( You've successfully accepted the contact. - Until confirmed, it will be displayed as .
+ Until confirmed, it will be displayed as .
You can confirm it right now or return to this process later by using the "Show code" button.
); diff --git a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx index 1490195..c43a1cc 100644 --- a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx @@ -1,4 +1,10 @@ -import { ContactAwaitingAcceptance, ContactConfirmed, ContactNotConfirmed, ContactStatus } from '@/api/types/contact'; +import { + ContactAwaitingAcceptance, + ContactConfirmed, + ContactNotConfirmed, + ContactStatus, + ContactRejected, +} from '@/api/types/contact'; import { FC } from 'react'; import { Chip, ChipProps } from '@mui/material'; @@ -7,7 +13,7 @@ type StatusBadgeProps = { }; export const StatusBadge: FC = ({ status }) => { - const { label, color } = contactStatuses[status]; + const { label, color } = contactStatuses[status] ?? { label: 'Unknown', color: 'error' }; return ; }; @@ -16,4 +22,5 @@ const contactStatuses: Record = ({ open, onSubmitted, o setLoading(true); setError(false); try { - addContact(paymail, name); + await upsertContact(paymail, name); onSuccess(); } catch (error) { + console.log('error', error); setError(true); } finally { setLoading(false); diff --git a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx index 47dcbdc..b10b751 100644 --- a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx @@ -13,7 +13,7 @@ type VerifyModalProps = { }; export const VerifyModal: FC> = ({ children, peer, onConfirmed, onClose }) => { - const { name, paymail, status } = peer; + const { fullName, paymail, status } = peer; const yourTOTP = useYourTOTP(paymail); const peerTOTP = usePeerTOTP(paymail, onConfirmed); @@ -24,7 +24,7 @@ export const VerifyModal: FC> = ({ children, return ( > = ({ children, > {children} - + {status === ContactConfirmed ? ( - {name} is your trusted contact. + {fullName} is your trusted contact. ) : ( - + )} From 1d0061767b777217d224b264bb4aaa6f609a8530 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:48:51 +0200 Subject: [PATCH 02/13] feat(SPV-679): modals for upserting a contact --- src/api/requests/contact.ts | 55 ++++----------- src/api/types/contact.ts | 12 +++- .../ContactsTable.tsx/ContactsTable.tsx | 67 ++++++++++-------- .../ContactsList/_modals/ContactAdd.tsx | 4 +- .../ContactsList/_modals/ContactEdit.tsx | 29 ++++++++ .../ContactUpsertModal/ContactAddModal.tsx | 24 +++++++ .../ContactUpsertModal/ContactEditModal.tsx | 34 ++++++++++ .../ContactUpsertModal.tsx} | 68 +++++++++++++------ .../_modals/ContactUpsertModal/index.ts | 2 + .../ContactUpsertModal/useContactFields.tsx | 18 +++++ .../_modals/VerifyModal/PeerTOTP.tsx | 6 +- .../_modals/VerifyModal/VerifyModal.tsx | 4 +- .../_modals/VerifyModal/YourTOTP.tsx | 8 +-- src/components/ContactsList/_modals/index.ts | 4 +- src/components/Input/Input.styles.ts | 2 +- src/components/Input/PaymailInput.tsx | 26 +++---- src/components/Input/types.ts | 1 + src/components/Modal/Modal.styles.ts | 2 + src/styles/colors.ts | 1 + src/styles/variables.ts | 10 +-- src/utils/helpers/validatePhone.ts | 5 ++ 21 files changed, 258 insertions(+), 124 deletions(-) create mode 100644 src/components/ContactsList/_modals/ContactEdit.tsx create mode 100644 src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx create mode 100644 src/components/ContactsList/_modals/ContactUpsertModal/ContactEditModal.tsx rename src/components/ContactsList/_modals/{ContactAddModal/ContactAddModal.tsx => ContactUpsertModal/ContactUpsertModal.tsx} (53%) create mode 100644 src/components/ContactsList/_modals/ContactUpsertModal/index.ts create mode 100644 src/components/ContactsList/_modals/ContactUpsertModal/useContactFields.tsx create mode 100644 src/utils/helpers/validatePhone.ts diff --git a/src/api/requests/contact.ts b/src/api/requests/contact.ts index 5849305..af96e5a 100644 --- a/src/api/requests/contact.ts +++ b/src/api/requests/contact.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { timeoutPromise } from '@/utils/timeoutPromise'; import { PaginationParams } from '../types'; -import { Contact } from '../types/contact'; +import { Contact, ContactMetadata } from '../types/contact'; import axios from 'axios'; export const searchContacts = async (_pagination?: PaginationParams) => { @@ -9,40 +8,30 @@ export const searchContacts = async (_pagination?: PaginationParams) => { return response; }; -export const upsertContact = async (paymail: string, fullName: string) => { +export const upsertContact = async (paymail: string, fullName: string, metadata?: ContactMetadata) => { await axios.put(`/contact/${encodeURIPaymail(paymail)}`, { fullName, + metadata, }); }; export const rejectContact = async (paymail: string) => { - console.log('reject'); - await timeoutPromise(1000); - contacts = contacts.filter((contact) => contact.paymail !== paymail); + await axios.patch(`/contact/rejected/${encodeURIPaymail(paymail)}`); }; export const acceptContact = async (paymail: string) => { - await timeoutPromise(1000); - contacts = contacts.map((contact) => { - if (contact.paymail === paymail) { - contact.status = 'unconfirmed'; - } - return { ...contact }; - }); + await axios.patch(`/contact/accepted/${encodeURIPaymail(paymail)}`); }; -export const getTOTP = async (_paymail: string) => { - await timeoutPromise(1000); - return Math.floor(10 + Math.random() * 89); +export const getTOTP = async (contact: Contact) => { + const res = await axios.post(`/contact/totp`, contact); + return res.data.passcode; }; -export const confirmContactWithTOTP = async (paymail: string, _totp: number) => { - await timeoutPromise(1000); - contacts = contacts.map((contact) => { - if (contact.paymail === paymail) { - contact.status = 'confirmed'; - } - return { ...contact }; +export const confirmContactWithTOTP = async (contact: Contact, totp: number) => { + await axios.patch(`/contact/confirmed`, { + passcode: totp, + contact, }); }; @@ -59,23 +48,3 @@ const encodeURIPaymail = (paymail: string) => { const sanitizedPaymail = String.fromCharCode(...iterator()); return encodeURIComponent(sanitizedPaymail); }; - -/// Mocked contacts - -let contacts: Contact[] = [ - { - paymail: 'bob@example.com', - fullName: 'Bob', - status: 'confirmed', - }, - { - paymail: 'tester@example.com', - fullName: 'Tester', - status: 'awaiting', - }, - { - paymail: 'theguy@example.com', - fullName: 'The Guy', - status: 'unconfirmed', - }, -]; diff --git a/src/api/types/contact.ts b/src/api/types/contact.ts index 047397a..1c3644f 100644 --- a/src/api/types/contact.ts +++ b/src/api/types/contact.ts @@ -10,9 +10,17 @@ export type ContactStatus = | typeof ContactRejected; export type Contact = { + created_at: string; + updated_at: string; + deleted_at: string; + metadata?: ContactMetadata; + id: string; + pubKey: string; paymail: string; - fullName: string; status: ContactStatus; + fullName: string; +}; - //TODO: Add more fields (...metadata) +export type ContactMetadata = { + phoneNumber?: string; }; diff --git a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx index c9c902d..6f91951 100644 --- a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx @@ -12,7 +12,7 @@ import { import { SetPaymailButton } from '@/components/TransferForm/SetPaymailButton'; import { FC, useMemo, useState } from 'react'; import { StatusBadge } from './StatusBadge'; -import { VerifyModal } from '../_modals'; +import { ContactEdit, VerifyModal } from '../_modals'; import { SmallButton } from '@/components/Button'; import { AcceptReject } from '../AcceptReject'; import { useContacts } from '@/providers'; @@ -57,37 +57,44 @@ export const ContactsTable: FC = () => { - {sortedContacts.map(({ paymail, status, fullName }) => ( - - {paymail} - {fullName} - - - - - {status !== ContactAwaitingAcceptance ? ( - { - openVerificationWindow(paymail); - }} - > - Show code - - ) : ( - { + const { paymail, fullName, status } = contact; + return ( + + {paymail} + {fullName} + + + + + + {status !== ContactAwaitingAcceptance ? ( + { + openVerificationWindow(paymail); + }} + > + Show code + + ) : ( + { + openVerificationWindow(paymail, true); + refresh(); + }} + onReject={refresh} + /> + )} + { - openVerificationWindow(paymail, true); - refresh(); - }} - onReject={refresh} + variant={status === ContactConfirmed ? 'accept' : 'primary'} /> - )} - - - - ))} + + + ); + })} )} diff --git a/src/components/ContactsList/_modals/ContactAdd.tsx b/src/components/ContactsList/_modals/ContactAdd.tsx index 7117f14..081af60 100644 --- a/src/components/ContactsList/_modals/ContactAdd.tsx +++ b/src/components/ContactsList/_modals/ContactAdd.tsx @@ -1,6 +1,6 @@ import { FC, useState } from 'react'; import { Button } from '@/components/Button'; -import { ContactAddModal } from './ContactAddModal/ContactAddModal'; +import { ContactAddModal } from './ContactUpsertModal'; import { useContacts } from '@/providers'; export const ContactAdd: FC = () => { @@ -17,7 +17,7 @@ export const ContactAdd: FC = () => { - {open && setOpen(false)} />} + {open && setOpen(false)} />} ); }; diff --git a/src/components/ContactsList/_modals/ContactEdit.tsx b/src/components/ContactsList/_modals/ContactEdit.tsx new file mode 100644 index 0000000..b893f53 --- /dev/null +++ b/src/components/ContactsList/_modals/ContactEdit.tsx @@ -0,0 +1,29 @@ +import { FC, useState } from 'react'; +import { SmallButton } from '@/components/Button'; +import { ContactEditModal } from './ContactUpsertModal'; +import { useContacts } from '@/providers'; +import { Contact } from '@/api'; +import EditIcon from '@mui/icons-material/Edit'; + +type ContactEditProps = { + contact: Contact; +}; + +export const ContactEdit: FC = ({ contact }) => { + const [open, setOpen] = useState(false); + const { refresh } = useContacts(); + + const onSubmitted = () => { + setOpen(false); + refresh(); + }; + + return ( + <> + setOpen(true)}> + + + {open && setOpen(false)} />} + + ); +}; diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx new file mode 100644 index 0000000..612c0d6 --- /dev/null +++ b/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; +import { ContactUpsertModal } from './ContactUpsertModal'; +import { useContactFields } from './useContactFields'; + +type ContactAddModalProps = { + onSubmitted: () => void; + onCancel: () => void; +}; + +export const ContactAddModal: FC = ({ onSubmitted, onCancel }) => { + const fields = useContactFields(); + return ( + + ); +}; diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/ContactEditModal.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/ContactEditModal.tsx new file mode 100644 index 0000000..974c833 --- /dev/null +++ b/src/components/ContactsList/_modals/ContactUpsertModal/ContactEditModal.tsx @@ -0,0 +1,34 @@ +import { FC, useEffect } from 'react'; +import { ContactUpsertModal } from './ContactUpsertModal'; +import { useContactFields } from './useContactFields'; +import { Contact } from '@/api'; + +type ContactEditModalProps = { + contact: Contact; + onSubmitted: () => void; + onCancel: () => void; +}; + +export const ContactEditModal: FC = ({ onSubmitted, onCancel, contact }) => { + const fields = useContactFields(); + const { setPaymail, setName, setPhone } = fields; + + useEffect(() => { + setPaymail(contact.paymail); + setName(contact.fullName); + setPhone(contact?.metadata?.phoneNumber ?? ''); + }, [contact, setName, setPaymail, setPhone]); + + return ( + + ); +}; diff --git a/src/components/ContactsList/_modals/ContactAddModal/ContactAddModal.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx similarity index 53% rename from src/components/ContactsList/_modals/ContactAddModal/ContactAddModal.tsx rename to src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx index f085af9..1c533b3 100644 --- a/src/components/ContactsList/_modals/ContactAddModal/ContactAddModal.tsx +++ b/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx @@ -7,36 +7,58 @@ import { Input } from '@/components/Input'; import { upsertContact } from '@/api/requests/contact'; import { modalCloseTimeout } from '@/components/Modal/modalCloseTimeout'; import { ErrorBar } from '@/components/ErrorBar'; +import { isValidPhone } from '@/utils/helpers/validatePhone'; +import { ContactFields } from './useContactFields'; -type ContactAddModalProps = { - open: boolean; +type ContactUpsertModal = { onSubmitted: () => void; onCancel: () => void; + sucessMsg: string; + errorMsg: string; + modalTitle: string; + modalSubtitle: string; + fields: ContactFields; + disabledPaymailInput: boolean; }; -export const ContactAddModal: FC = ({ open, onSubmitted, onCancel }) => { - const [paymail, setPaymail] = useState(''); - const [name, setName] = useState(''); +export const ContactUpsertModal: FC = ({ + onSubmitted, + onCancel, + fields, + errorMsg, + sucessMsg, + modalTitle, + modalSubtitle, + disabledPaymailInput, +}) => { const [loading, setLoading] = useState(false); - const [error, setError] = useState(false); - + const [error, setError] = useState(undefined); const [successMessage, setSuccessMessage] = useState(''); + const { name, paymail, phone, setName, setPaymail, setPhone } = fields; const onSuccess = async () => { - setSuccessMessage('Contact added successfully!'); + setSuccessMessage(sucessMsg); await modalCloseTimeout(); onSubmitted(); }; const onSubmit = async () => { + if (phone && !isValidPhone(phone)) { + setError('Wrong phone number.'); + return; + } + if (!paymail || !name) { + setError('Both Paymail and Name are required'); + return; + } setLoading(true); - setError(false); + setError(undefined); try { - await upsertContact(paymail, name); + await upsertContact(paymail, name, phone ? { phoneNumber: phone } : undefined); onSuccess(); } catch (error) { - console.log('error', error); - setError(true); + console.error('error', error); + setError(errorMsg); } finally { setLoading(false); } @@ -44,12 +66,12 @@ export const ContactAddModal: FC = ({ open, onSubmitted, o return ( = ({ open, onSubmitted, o onCloseByEsc={onCancel} > {loading && } - {error && } + {error && }
- Add contact form + {modalTitle}
setPaymail(event.target.value)} + labelSuffix="*" required + disabled={disabledPaymailInput} /> setName(event.target.value)} required /> + setPhone(event.target.value)} + />
diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/index.ts b/src/components/ContactsList/_modals/ContactUpsertModal/index.ts new file mode 100644 index 0000000..6ea2bc6 --- /dev/null +++ b/src/components/ContactsList/_modals/ContactUpsertModal/index.ts @@ -0,0 +1,2 @@ +export * from './ContactAddModal'; +export * from './ContactEditModal'; diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/useContactFields.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/useContactFields.tsx new file mode 100644 index 0000000..c7f17e8 --- /dev/null +++ b/src/components/ContactsList/_modals/ContactUpsertModal/useContactFields.tsx @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +export const useContactFields = () => { + const [paymail, setPaymail] = useState(''); + const [name, setName] = useState(''); + const [phone, setPhone] = useState(''); + + return { + paymail, + setPaymail, + name, + setName, + phone, + setPhone, + }; +}; + +export type ContactFields = ReturnType; diff --git a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx index f3cf9fe..b34b5bc 100644 --- a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx @@ -1,11 +1,11 @@ -import { confirmContactWithTOTP } from '@/api'; +import { Contact, confirmContactWithTOTP } from '@/api'; import { ErrorBar } from '@/components/ErrorBar'; import { Input } from '@/components/Input'; import { FC, useMemo, useState } from 'react'; const TOTP_VALID_REGEX = /^\d{2}$/; -export const usePeerTOTP = (peerPaymail: string, onConfirmed: () => void) => { +export const usePeerTOTP = (peer: Contact, onConfirmed: () => void) => { const [value, setValue] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -18,7 +18,7 @@ export const usePeerTOTP = (peerPaymail: string, onConfirmed: () => void) => { setLoading(true); setError(false); try { - await confirmContactWithTOTP(peerPaymail, parseInt(value)); + await confirmContactWithTOTP(peer, parseInt(value)); } catch { setError(true); } finally { diff --git a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx index b10b751..36cc592 100644 --- a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx @@ -14,8 +14,8 @@ type VerifyModalProps = { export const VerifyModal: FC> = ({ children, peer, onConfirmed, onClose }) => { const { fullName, paymail, status } = peer; - const yourTOTP = useYourTOTP(paymail); - const peerTOTP = usePeerTOTP(paymail, onConfirmed); + const yourTOTP = useYourTOTP(peer); + const peerTOTP = usePeerTOTP(peer, onConfirmed); if (status == ContactAwaitingAcceptance) { return null; diff --git a/src/components/ContactsList/_modals/VerifyModal/YourTOTP.tsx b/src/components/ContactsList/_modals/VerifyModal/YourTOTP.tsx index 494e260..36b5df8 100644 --- a/src/components/ContactsList/_modals/VerifyModal/YourTOTP.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/YourTOTP.tsx @@ -2,10 +2,10 @@ import { FC, useCallback, useEffect, useState } from 'react'; import styled from '@emotion/styled'; import { variables } from '@/styles'; import { colors, sizes } from '@/styles'; -import { getTOTP } from '@/api'; +import { Contact, getTOTP } from '@/api'; import { CircularProgress } from '@mui/material'; -export const useYourTOTP = (peerPaymai: string) => { +export const useYourTOTP = (peer: Contact) => { const [totp, setTotp] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -14,13 +14,13 @@ export const useYourTOTP = (peerPaymai: string) => { setLoading(true); setError(false); try { - setTotp(await getTOTP(peerPaymai)); + setTotp(await getTOTP(peer)); } catch { setError(true); } finally { setLoading(false); } - }, [peerPaymai]); + }, [peer]); useEffect(() => { fetchTotp(); diff --git a/src/components/ContactsList/_modals/index.ts b/src/components/ContactsList/_modals/index.ts index 3ab09e2..30befdc 100644 --- a/src/components/ContactsList/_modals/index.ts +++ b/src/components/ContactsList/_modals/index.ts @@ -1,2 +1,4 @@ -export * from './ContactAddModal/ContactAddModal'; +export * from './ContactUpsertModal/ContactUpsertModal'; export * from './VerifyModal/VerifyModal'; +export * from './ContactAdd'; +export * from './ContactEdit'; diff --git a/src/components/Input/Input.styles.ts b/src/components/Input/Input.styles.ts index 6cfe86f..7d0c6ac 100644 --- a/src/components/Input/Input.styles.ts +++ b/src/components/Input/Input.styles.ts @@ -46,7 +46,7 @@ export const InputStyled = styled.input` border: 2px solid ${({ inputOnLightBackground }) => (inputOnLightBackground ? colors.darkPrimary : colors.lightPrimary)}; border-radius: ${variables.doubleBorderRadius}; - background-color: ${colors.inputBackground}; + background-color: ${({ disabled }) => (disabled ? colors.disabledInputBackground : colors.inputBackground)}; color: ${({ inputOnLightBackground }) => (inputOnLightBackground ? colors.darkPrimary : colors.lightPrimary)}; ${media.sm} { diff --git a/src/components/Input/PaymailInput.tsx b/src/components/Input/PaymailInput.tsx index 0f080e8..4587b3c 100644 --- a/src/components/Input/PaymailInput.tsx +++ b/src/components/Input/PaymailInput.tsx @@ -6,18 +6,20 @@ import { usePaymailDomain } from '@/hooks/usePaymailDomain'; import { InputLinkButton } from './Input.styles'; import ContactsIcon from '@mui/icons-material/Contacts'; -export const PaymailInput = forwardRef(({ showContactsButton, ...props }, ref) => { - const paymailDomain = usePaymailDomain(); +export const PaymailInput = forwardRef( + ({ showContactsButton, labelSuffix = '', ...props }, ref) => { + const paymailDomain = usePaymailDomain(); - return ( - - {showContactsButton && ( - - - - )} - - ); -}); + return ( + + {showContactsButton && ( + + + + )} + + ); + }, +); PaymailInput.displayName = 'PaymailInput'; diff --git a/src/components/Input/types.ts b/src/components/Input/types.ts index f2f45de..8cbd9c5 100644 --- a/src/components/Input/types.ts +++ b/src/components/Input/types.ts @@ -15,4 +15,5 @@ export type CoinsInputProps = Omit; export type PaymailInputProps = { showContactsButton?: boolean; + labelSuffix?: string; } & Omit; diff --git a/src/components/Modal/Modal.styles.ts b/src/components/Modal/Modal.styles.ts index 90b2508..53ce5ff 100644 --- a/src/components/Modal/Modal.styles.ts +++ b/src/components/Modal/Modal.styles.ts @@ -57,6 +57,7 @@ export const ModalMainContent = styled.main` flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; + padding: ${sizes(4)}; `; export const ModalHeadline = styled.h3` @@ -76,6 +77,7 @@ export const ButtonsWrapper = styled.footer` justify-content: center; margin: ${sizes(12)} 0 ${sizes(6)}; z-index: 1; + padding: ${sizes(4)}; `; export const ModalButton = styled(Button)` diff --git a/src/styles/colors.ts b/src/styles/colors.ts index ddf511c..89d3a75 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -8,6 +8,7 @@ export const colors = { userMenuBackground: 'rgba(255, 255, 255, 0.4)', dashboardTileBackground: 'rgba(0, 0, 0, 0.6)', modalWrapperBackground: 'rgba(255, 255, 255, 0.4)', + disabledInputBackground: 'rgba(180, 180, 180, 1)', primary: '#13145e', lightPrimary: '#ffffff', darkPrimary: '#000', diff --git a/src/styles/variables.ts b/src/styles/variables.ts index a6a4e27..6712edc 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -14,11 +14,11 @@ export const variables = { }, shadow: { tileShadow: '0 0 40px rgba(8, 7, 16, 0.6)', - buttonShadow: '0 5px 15px rgba(0, 0, 0, 0.3)', - buttonEffectShadow: '0 5px 35px rgba(0, 0, 0, 0.3)', - smallButtonShadow: '0 1px 3px rgba(0, 0, 0, 0.3)', - smallButtonEffectShadow: '0 1px 7px rgba(0, 0, 0, 0.3)', - inputEffectShadow: '0 5px 15px rgba(0, 0, 0, 0.3)', + buttonShadow: '0 5px 5px rgba(0, 0, 0, 0.15)', + buttonEffectShadow: '0 5px 15px rgba(0, 0, 0, 0.15)', + smallButtonShadow: '0 1px 3px rgba(0, 0, 0, 0.15)', + smallButtonEffectShadow: '0 1px 7px rgba(0, 0, 0, 0.15)', + inputEffectShadow: '0 5px 15px rgba(0, 0, 0, 0.15)', }, transition: { baseEffect: 'all 0.2s ease-in-out', diff --git a/src/utils/helpers/validatePhone.ts b/src/utils/helpers/validatePhone.ts new file mode 100644 index 0000000..fea7ea8 --- /dev/null +++ b/src/utils/helpers/validatePhone.ts @@ -0,0 +1,5 @@ +const phoneRegex = /^\+?([0-9]{1,3})\)?[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})[-. ]?([0-9]{1,4})$/; + +export const isValidPhone = (phone: string) => { + return phoneRegex.test(phone); +}; From a823206140fc142488d3b26fbb0a72c0ae30790b Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:06:42 +0200 Subject: [PATCH 03/13] feat(SPV-679): styles --- .../ContactsTable.tsx/ContactsTable.tsx | 5 ++++- src/styles/index.ts | 1 + src/styles/textWrapper.tsx | 8 ++++++++ src/views/Contacts/Contacts.tsx | 13 +++++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/styles/textWrapper.tsx diff --git a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx index 6f91951..984727d 100644 --- a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx @@ -19,6 +19,7 @@ import { useContacts } from '@/providers'; import { ErrorBar } from '@/components/ErrorBar'; import { useSortedContacts } from './useSortedContacts'; import { JustAddedContactMsg } from './JustAddedContcatMsg'; +import { TextWrapper } from '@/styles'; export const ContactsTable: FC = () => { const { contacts, loading, error, refresh } = useContacts(); @@ -61,7 +62,9 @@ export const ContactsTable: FC = () => { const { paymail, fullName, status } = contact; return ( - {paymail} + + {paymail} + {fullName} diff --git a/src/styles/index.ts b/src/styles/index.ts index f13c669..7e343e6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5,3 +5,4 @@ export * from './sizes'; export * from './breakpoints'; export * from './keyframes'; export * from './variables'; +export * from './textWrapper'; diff --git a/src/styles/textWrapper.tsx b/src/styles/textWrapper.tsx new file mode 100644 index 0000000..b8eff71 --- /dev/null +++ b/src/styles/textWrapper.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const TextWrapper = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +`; diff --git a/src/views/Contacts/Contacts.tsx b/src/views/Contacts/Contacts.tsx index 8409cce..a625e7f 100644 --- a/src/views/Contacts/Contacts.tsx +++ b/src/views/Contacts/Contacts.tsx @@ -4,6 +4,8 @@ import { AccountSummary } from '@/components/AccountSummary'; import { TransferForm } from '@/components/TransferForm'; import { ContactsList } from '@/components/ContactsList'; import { useWebsocket } from '@/hooks'; +import styled from '@emotion/styled'; +import { sizes } from '@/styles'; export const Contacts = () => { const lgMatch = useMediaMatch('lg'); @@ -16,10 +18,17 @@ export const Contacts = () => { - - + + + + ); }; + +const StickyColumn = styled('div')` + position: sticky; + top: ${sizes(4)}; +`; From 92ae1ea64d7895b0696adb929ab356462a9de889 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:25:19 +0200 Subject: [PATCH 04/13] feat(SPV-679): showing a badge for input paymail with status in the contacts --- .../ContactsTable.tsx/StatusBadge.tsx | 21 ++++++++-- src/components/Input/PaymailInput.tsx | 3 +- src/components/TransferForm/TransferForm.tsx | 39 +++++++++++++++++-- src/providers/contacts/provider.tsx | 6 ++- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx index c43a1cc..36ac88e 100644 --- a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx @@ -7,20 +7,33 @@ import { } from '@/api/types/contact'; import { FC } from 'react'; import { Chip, ChipProps } from '@mui/material'; +import styled from '@emotion/styled'; type StatusBadgeProps = { - status: ContactStatus; + status: ContactStatus | 'unknown'; }; -export const StatusBadge: FC = ({ status }) => { +export const StatusBadge: FC> = ({ status, ...props }) => { const { label, color } = contactStatuses[status] ?? { label: 'Unknown', color: 'error' }; - return ; + return ( + + + + ); }; -const contactStatuses: Record = { +const contactStatuses: Record = { [ContactAwaitingAcceptance]: { label: 'Pending', color: 'primary' }, [ContactNotConfirmed]: { label: 'Untrusted', color: 'secondary' }, [ContactConfirmed]: { label: 'Trusted', color: 'success' }, [ContactRejected]: { label: 'Rejected', color: 'error' }, + unknown: { label: 'Unknown', color: 'warning' }, }; + +const ChipContainer = styled.div` + & div span { + line-height: 1; + transform: translateY(1.5px); + } +`; diff --git a/src/components/Input/PaymailInput.tsx b/src/components/Input/PaymailInput.tsx index 4587b3c..3fd3321 100644 --- a/src/components/Input/PaymailInput.tsx +++ b/src/components/Input/PaymailInput.tsx @@ -7,7 +7,7 @@ import { InputLinkButton } from './Input.styles'; import ContactsIcon from '@mui/icons-material/Contacts'; export const PaymailInput = forwardRef( - ({ showContactsButton, labelSuffix = '', ...props }, ref) => { + ({ showContactsButton, labelSuffix = '', children, ...props }, ref) => { const paymailDomain = usePaymailDomain(); return ( @@ -17,6 +17,7 @@ export const PaymailInput = forwardRef( )} + {children} ); }, diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index 3a33e1f..9763904 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -1,9 +1,9 @@ import { DashboardTile } from '@/components/DashboardTile'; import SendIcon from '@mui/icons-material/Send'; import { Button } from '@/components/Button'; -import { SrOnlySpan } from '@/styles'; +import { SrOnlySpan, sizes } from '@/styles'; import { Column, Row } from '@/styles/grid'; -import { ChangeEvent, FormEvent, useCallback, useState, FC } from 'react'; +import { ChangeEvent, FormEvent, useCallback, useState, FC, useEffect } from 'react'; import { Loader } from '@/components/Loader'; import { TransactionConfirmModal, TransactionData } from '@/components/Modal/_modals/TransactionConfirmModal'; import { EMAIL_REGEX } from '@/utils/constants'; @@ -13,6 +13,11 @@ import { CoinsInput } from '../Input/CoinsInput'; import { PaymailInput } from '../Input/PaymailInput'; import { useSubscribePaymailEvent } from './setPaymailEvent'; import { usePaymailInputAnimation } from './paymailInputAnimation'; +import { debounce } from 'lodash'; +import { useContacts } from '@/providers'; +import { ContactStatus } from '@/api'; +import { StatusBadge } from '../ContactsList/ContactsTable.tsx/StatusBadge'; +import styled from '@emotion/styled'; type TransferFormProps = { showContactsButton?: boolean; @@ -85,6 +90,26 @@ export const TransferForm: FC = ({ showContactsButton }) => { setAmount(value); } }; + + const { contacts } = useContacts(); + const [paymailStatus, setPaymailStatus] = useState(); + const checkPaymailInContacts = useCallback( + (value: string) => { + if (!value) { + setPaymailStatus(undefined); + return; + } + value = value.toLowerCase(); + const found = contacts?.find((el) => el.paymail === value); + setPaymailStatus(found?.status ?? 'unknown'); + }, + [contacts], + ); + const debouncedValidateInput = debounce(checkPaymailInContacts, 500); + useEffect(() => { + debouncedValidateInput(paymail); + }, [debouncedValidateInput, paymail]); + return ( }> {loading && } @@ -95,13 +120,17 @@ export const TransferForm: FC = ({ showContactsButton }) => { Money transfer form - setPaymail(event.target.value)} value={paymail} showContactsButton={showContactsButton} /> + {errors && } @@ -139,3 +168,7 @@ export const TransferForm: FC = ({ showContactsButton }) => { ); }; + +const StyledPaymailInput = styled(PaymailInput)` + margin-bottom: ${sizes(1)}; +`; diff --git a/src/providers/contacts/provider.tsx b/src/providers/contacts/provider.tsx index bab35a1..d83e12c 100644 --- a/src/providers/contacts/provider.tsx +++ b/src/providers/contacts/provider.tsx @@ -2,6 +2,7 @@ import { searchContacts } from '@/api/requests/contact'; import { Contact } from '@/api/types/contact'; import { usePikeEnabled } from '@/hooks/useFeatureFlags'; import { FC, PropsWithChildren, createContext, useCallback, useEffect, useMemo, useState } from 'react'; +import { useAuthorization } from '../authorization'; type ContactsContextValue = { contacts: Contact[] | null; @@ -17,9 +18,10 @@ export const ContactsProvider: FC = ({ children }) => { const [error, setError] = useState(false); const [contacts, setContacts] = useState(null); const enabled = usePikeEnabled(); + const { authorization } = useAuthorization(); const load = useCallback(async () => { - if (!enabled) { + if (!enabled || !authorization) { return; } setLoading(true); @@ -32,7 +34,7 @@ export const ContactsProvider: FC = ({ children }) => { } finally { setLoading(false); } - }, [enabled]); + }, [authorization, enabled]); const refresh = useCallback(() => { load(); From 35d9d43c527d9091669a0e790d89372a1faa3917 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:08:52 +0200 Subject: [PATCH 05/13] feat(SPV-679): totp --- src/api/requests/contact.ts | 2 +- src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/requests/contact.ts b/src/api/requests/contact.ts index af96e5a..cc81feb 100644 --- a/src/api/requests/contact.ts +++ b/src/api/requests/contact.ts @@ -28,7 +28,7 @@ export const getTOTP = async (contact: Contact) => { return res.data.passcode; }; -export const confirmContactWithTOTP = async (contact: Contact, totp: number) => { +export const confirmContactWithTOTP = async (contact: Contact, totp: string) => { await axios.patch(`/contact/confirmed`, { passcode: totp, contact, diff --git a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx index b34b5bc..397e9e7 100644 --- a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx @@ -3,7 +3,7 @@ import { ErrorBar } from '@/components/ErrorBar'; import { Input } from '@/components/Input'; import { FC, useMemo, useState } from 'react'; -const TOTP_VALID_REGEX = /^\d{2}$/; +const TOTP_VALID_REGEX = /^\d{4}$/; export const usePeerTOTP = (peer: Contact, onConfirmed: () => void) => { const [value, setValue] = useState(''); @@ -18,7 +18,7 @@ export const usePeerTOTP = (peer: Contact, onConfirmed: () => void) => { setLoading(true); setError(false); try { - await confirmContactWithTOTP(peer, parseInt(value)); + await confirmContactWithTOTP(peer, value); } catch { setError(true); } finally { From b8820ce0f5d747e0f83dbf2a8e90009fe0462e2f Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:30:25 +0200 Subject: [PATCH 06/13] feat(SPV-679): ux adjustments --- .../ContactsTable.tsx/ContactsTable.tsx | 2 +- .../ContactsTable.tsx/JustAddedContcatMsg.tsx | 4 +++- .../ContactsTable.tsx/StatusBadge.tsx | 1 - .../ContactUpsertModal/ContactEditModal.tsx | 2 +- .../_modals/VerifyModal/VerifyModal.tsx | 1 - src/components/TransferForm/TransferForm.tsx | 18 +++++++++++------- src/styles/colors.ts | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx index 984727d..e70a30f 100644 --- a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx @@ -70,7 +70,7 @@ export const ContactsTable: FC = () => { - + {status !== ContactAwaitingAcceptance && } {status !== ContactAwaitingAcceptance ? ( { return ( You've successfully accepted the contact. - Until confirmed, it will be displayed as .
+ Until confirmed, it will be displayed as +
You can confirm it right now or return to this process later by using the "Show code" button.
); @@ -15,6 +16,7 @@ export const JustAddedContactMsg: FC = () => { const Container = styled.div` padding: 20px; + border-top: 1px solid rgba(0, 0, 0, 0.1); `; const SuccessInfo = styled.div` diff --git a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx index 36ac88e..45c60a4 100644 --- a/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx @@ -34,6 +34,5 @@ const contactStatuses: Record = ({ onSubmitted, onCan modalTitle="Edit contact" modalSubtitle="" fields={fields} - disabledPaymailInput={false} + disabledPaymailInput={true} /> ); }; diff --git a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx index 36cc592..60b3b97 100644 --- a/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/VerifyModal.tsx @@ -59,7 +59,6 @@ export const VerifyModal: FC> = ({ children, }; const Container = styled.div` - padding: 20px; max-width: 80vw; width: 800px; `; diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index 9763904..13b9894 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -3,7 +3,7 @@ import SendIcon from '@mui/icons-material/Send'; import { Button } from '@/components/Button'; import { SrOnlySpan, sizes } from '@/styles'; import { Column, Row } from '@/styles/grid'; -import { ChangeEvent, FormEvent, useCallback, useState, FC, useEffect } from 'react'; +import { ChangeEvent, FormEvent, useCallback, useState, FC, useEffect, useMemo } from 'react'; import { Loader } from '@/components/Loader'; import { TransactionConfirmModal, TransactionData } from '@/components/Modal/_modals/TransactionConfirmModal'; import { EMAIL_REGEX } from '@/utils/constants'; @@ -95,7 +95,7 @@ export const TransferForm: FC = ({ showContactsButton }) => { const [paymailStatus, setPaymailStatus] = useState(); const checkPaymailInContacts = useCallback( (value: string) => { - if (!value) { + if (!value || !value.match(EMAIL_REGEX)) { setPaymailStatus(undefined); return; } @@ -105,7 +105,7 @@ export const TransferForm: FC = ({ showContactsButton }) => { }, [contacts], ); - const debouncedValidateInput = debounce(checkPaymailInContacts, 500); + const debouncedValidateInput = useMemo(() => debounce(checkPaymailInContacts, 1000), [checkPaymailInContacts]); useEffect(() => { debouncedValidateInput(paymail); }, [debouncedValidateInput, paymail]); @@ -127,10 +127,14 @@ export const TransferForm: FC = ({ showContactsButton }) => { value={paymail} showContactsButton={showContactsButton} /> - +
+ {paymailStatus != null && ( + <> + This is contact + + )} +
+ {errors && } diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 89d3a75..aa39409 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -8,7 +8,7 @@ export const colors = { userMenuBackground: 'rgba(255, 255, 255, 0.4)', dashboardTileBackground: 'rgba(0, 0, 0, 0.6)', modalWrapperBackground: 'rgba(255, 255, 255, 0.4)', - disabledInputBackground: 'rgba(180, 180, 180, 1)', + disabledInputBackground: 'rgba(200, 200, 200, 1)', primary: '#13145e', lightPrimary: '#ffffff', darkPrimary: '#000', From 4a17dca8d9557e8a45ecbdf925457ffc9365d013 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:58:58 +0200 Subject: [PATCH 07/13] feat(SPV-679): text adjustment --- src/components/TransferForm/TransferForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index 13b9894..07691d7 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -130,7 +130,8 @@ export const TransferForm: FC = ({ showContactsButton }) => {
{paymailStatus != null && ( <> - This is contact + This is {' '} + {paymailStatus !== 'unknown' ? 'contact' : 'paymail'} )}
From 9e5d27cae8c44d847096020725134a2e57414e78 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:21:46 +0200 Subject: [PATCH 08/13] feat(SPV-679): autocomplete --- src/components/Input/Input.tsx | 4 +- src/components/Input/PaymailAutocomplete.tsx | 88 ++++++++++++++++++++ src/components/Input/types.ts | 9 +- src/components/TransferForm/TransferForm.tsx | 10 +-- 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 src/components/Input/PaymailAutocomplete.tsx diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 1e4f2c7..217f1a7 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -4,9 +4,9 @@ import { InputStyled, InputWrapper, LabelStyled } from './Input.styles'; import { InputProps } from './types'; export const Input = forwardRef( - ({ labelText, className, id, customPlaceholder, inputOnLightBackground, children, ...rest }, ref) => { + ({ labelText, className, id, customPlaceholder, inputOnLightBackground, children, rootProps, ...rest }, ref) => { return ( - + ( + ({ children, paymailValue, onPaymailChange, ...props }, ref) => { + const { contacts } = useContacts(); + + const contactsPaymails = useMemo(() => contacts?.map((c) => c.paymail) ?? [], [contacts]); + + const isSelected = useMemo( + () => contactsPaymails.find((c) => c === paymailValue), + [contactsPaymails, paymailValue], + ); + + const { getRootProps, getInputProps, getListboxProps, getOptionProps, groupedOptions } = useAutocomplete({ + id: 'paymail-autocomplete', + options: !isSelected && paymailValue != '' ? contactsPaymails : [], + autoHighlight: true, + inputValue: paymailValue, + onInputChange: (_, value) => { + onPaymailChange(value); + }, + freeSolo: true, + openOnFocus: false, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onMouseDown: _, ...rootProps } = getRootProps(); //omit onMouseDown to allow a free select of text in the input + + return ( + + {children} + + {groupedOptions.length > 0 ? ( + + {groupedOptions.map((contactPaymail, index) => { + const p = contactPaymail as string; + return ( +
  • + {p} +
  • + ); + })} +
    + ) : null} +
    + ); + }, +); + +PaymailAutocomplete.displayName = 'PaymailAutocomplete'; + +const Listbox = styled('ul')(() => ({ + width: '100%', + margin: 0, + padding: 0, + zIndex: 1, + position: 'absolute', + listStyle: 'none', + backgroundColor: colors.primaryBackground, + overflowX: 'hidden', + overflowY: 'auto', + scrollbarWidth: 'thin', + scrollbarColor: `${colors.secondaryBackground} ${colors.primaryBackground}`, + boxShadow: '0 2px 4px rgba(0,0,0,.25)', + borderRadius: sizes(1), + + maxHeight: 200, + border: '1px solid rgba(0,0,0,.25)', + '& li.Mui-focused': { + backgroundColor: '#4a8df6', + color: 'white', + cursor: 'pointer', + }, + '& li:active': { + backgroundColor: '#2977f5', + color: 'white', + }, + '& li': { + padding: sizes(1), + }, +})); diff --git a/src/components/Input/types.ts b/src/components/Input/types.ts index 8cbd9c5..78bfb24 100644 --- a/src/components/Input/types.ts +++ b/src/components/Input/types.ts @@ -1,4 +1,4 @@ -import { InputHTMLAttributes } from 'react'; +import { HTMLAttributes, InputHTMLAttributes } from 'react'; export type InputProps = { labelText: string; @@ -7,6 +7,7 @@ export type InputProps = { withIcon?: boolean; customPlaceholder?: string; inputOnLightBackground?: boolean; + rootProps?: HTMLAttributes; } & InputHTMLAttributes; export type PasswordInputProps = Omit; @@ -16,4 +17,10 @@ export type CoinsInputProps = Omit; export type PaymailInputProps = { showContactsButton?: boolean; labelSuffix?: string; + rootProps?: HTMLAttributes; } & Omit; + +export type PaymailAutocompleteProps = { + paymailValue?: string; + onPaymailChange: (value: string) => void; +} & Omit; diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index 07691d7..0f73a46 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -10,7 +10,6 @@ import { EMAIL_REGEX } from '@/utils/constants'; import { ErrorBar } from '@/components/ErrorBar'; import { convertSatToBsv } from '@/utils/helpers/convertSatToBsv'; import { CoinsInput } from '../Input/CoinsInput'; -import { PaymailInput } from '../Input/PaymailInput'; import { useSubscribePaymailEvent } from './setPaymailEvent'; import { usePaymailInputAnimation } from './paymailInputAnimation'; import { debounce } from 'lodash'; @@ -18,6 +17,7 @@ import { useContacts } from '@/providers'; import { ContactStatus } from '@/api'; import { StatusBadge } from '../ContactsList/ContactsTable.tsx/StatusBadge'; import styled from '@emotion/styled'; +import { PaymailAutocomplete } from '../Input/PaymailAutocomplete'; type TransferFormProps = { showContactsButton?: boolean; @@ -120,11 +120,11 @@ export const TransferForm: FC = ({ showContactsButton }) => { Money transfer form - setPaymail(event.target.value)} - value={paymail} + onPaymailChange={(value) => setPaymail(value)} + paymailValue={paymail} showContactsButton={showContactsButton} />
    @@ -174,6 +174,6 @@ export const TransferForm: FC = ({ showContactsButton }) => { ); }; -const StyledPaymailInput = styled(PaymailInput)` +const StyledPaymailAutocomplete = styled(PaymailAutocomplete)` margin-bottom: ${sizes(1)}; `; From e83da8f0e0cd8837f651d065de56ff248b8a82f6 Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:16:50 +0200 Subject: [PATCH 09/13] feat(SPV-679): styles --- src/components/TransferForm/TransferForm.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index 0f73a46..ea4d9d3 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -127,14 +127,14 @@ export const TransferForm: FC = ({ showContactsButton }) => { paymailValue={paymail} showContactsButton={showContactsButton} /> -
    + {paymailStatus != null && ( <> This is {' '} {paymailStatus !== 'unknown' ? 'contact' : 'paymail'} )} -
    + @@ -177,3 +177,10 @@ export const TransferForm: FC = ({ showContactsButton }) => { const StyledPaymailAutocomplete = styled(PaymailAutocomplete)` margin-bottom: ${sizes(1)}; `; + +const StyledStatusWrapper = styled.div` + height: 15px; + margin-bottom: 30px; + padding-right: 20px; + text-align: right; +`; From 47af0f5096665334f279be3cca6b38c344e0d92f Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:43:13 +0200 Subject: [PATCH 10/13] feat(SPV-679): not showing paymailstatus if pikeEnabled is false --- src/components/TransferForm/TransferForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/TransferForm/TransferForm.tsx b/src/components/TransferForm/TransferForm.tsx index ea4d9d3..a5eb6b8 100644 --- a/src/components/TransferForm/TransferForm.tsx +++ b/src/components/TransferForm/TransferForm.tsx @@ -18,6 +18,7 @@ import { ContactStatus } from '@/api'; import { StatusBadge } from '../ContactsList/ContactsTable.tsx/StatusBadge'; import styled from '@emotion/styled'; import { PaymailAutocomplete } from '../Input/PaymailAutocomplete'; +import { usePikeEnabled } from '@/hooks/useFeatureFlags'; type TransferFormProps = { showContactsButton?: boolean; @@ -31,6 +32,7 @@ export const TransferForm: FC = ({ showContactsButton }) => { const [loading, setLoading] = useState(false); const [transactionData, setTransactionData] = useState(null); const [errors, setErrors] = useState(''); + const pikeEnabled = usePikeEnabled(); const sendButtonDisabled = !paymail || !amount; const cancelButtonDisabled = !paymail && !amount; @@ -128,7 +130,7 @@ export const TransferForm: FC = ({ showContactsButton }) => { showContactsButton={showContactsButton} /> - {paymailStatus != null && ( + {pikeEnabled && paymailStatus != null && ( <> This is {' '} {paymailStatus !== 'unknown' ? 'contact' : 'paymail'} From 4bf1d97761a701352d035152f3608f760bde480d Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:23:30 +0200 Subject: [PATCH 11/13] feat(SPV-679): adjust to the review --- .../_modals/ContactUpsertModal/ContactUpsertModal.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx index 1c533b3..c1025be 100644 --- a/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx +++ b/src/components/ContactsList/_modals/ContactUpsertModal/ContactUpsertModal.tsx @@ -9,6 +9,7 @@ import { modalCloseTimeout } from '@/components/Modal/modalCloseTimeout'; import { ErrorBar } from '@/components/ErrorBar'; import { isValidPhone } from '@/utils/helpers/validatePhone'; import { ContactFields } from './useContactFields'; +import { EMAIL_REGEX } from '@/utils/constants'; type ContactUpsertModal = { onSubmitted: () => void; @@ -32,7 +33,7 @@ export const ContactUpsertModal: FC = ({ disabledPaymailInput, }) => { const [loading, setLoading] = useState(false); - const [error, setError] = useState(undefined); + const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(''); const { name, paymail, phone, setName, setPaymail, setPhone } = fields; @@ -51,8 +52,12 @@ export const ContactUpsertModal: FC = ({ setError('Both Paymail and Name are required'); return; } + if (!paymail.match(EMAIL_REGEX)) { + setError('Invalid paymail address!'); + return; + } setLoading(true); - setError(undefined); + setError(null); try { await upsertContact(paymail, name, phone ? { phoneNumber: phone } : undefined); onSuccess(); From 7e6b0aa8e7df8c0fd3b2bb8d522d5c5f7a6b59ef Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:42:37 +0200 Subject: [PATCH 12/13] feat(SPV-679): two digits totp --- src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx index 397e9e7..da5ed9d 100644 --- a/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx +++ b/src/components/ContactsList/_modals/VerifyModal/PeerTOTP.tsx @@ -3,7 +3,7 @@ import { ErrorBar } from '@/components/ErrorBar'; import { Input } from '@/components/Input'; import { FC, useMemo, useState } from 'react'; -const TOTP_VALID_REGEX = /^\d{4}$/; +const TOTP_VALID_REGEX = /^\d{2}$/; export const usePeerTOTP = (peer: Contact, onConfirmed: () => void) => { const [value, setValue] = useState(''); From 89088e714a5910311239a2014101810bd05f2e5a Mon Sep 17 00:00:00 2001 From: Krzysztof Tomecki <152964795+chris-4chain@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:44:59 +0200 Subject: [PATCH 13/13] feat(SPV-679): minor adjustments --- .../ContactsList/ContactsTable.tsx/ContactsTable.tsx | 4 ++-- .../ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx | 4 +++- .../_modals/ContactUpsertModal/ContactAddModal.tsx | 2 +- .../_modals/ContactUpsertModal/ContactEditModal.tsx | 2 +- .../_modals/ContactUpsertModal/ContactUpsertModal.tsx | 6 +++--- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx index e70a30f..1663ff9 100644 --- a/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx @@ -1,4 +1,4 @@ -import { ContactAwaitingAcceptance, ContactConfirmed } from '@/api/types/contact'; +import { ContactAwaitingAcceptance, ContactConfirmed, ContactNotConfirmed } from '@/api/types/contact'; import { Loader } from '@/components/Loader'; import { LargeTd, @@ -111,7 +111,7 @@ export const ContactsTable: FC = () => { setJustAddedContact(false); }} > - {justAddedContact && contactForVerification.status === 'unconfirmed' && } + {justAddedContact && contactForVerification.status === ContactNotConfirmed && } )} diff --git a/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx b/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx index a01a1cd..866c084 100644 --- a/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx +++ b/src/components/ContactsList/ContactsTable.tsx/JustAddedContcatMsg.tsx @@ -2,12 +2,14 @@ import { FC } from 'react'; import styled from '@emotion/styled'; import { colors } from '@/styles'; import { StatusBadge } from './StatusBadge'; +import { ContactNotConfirmed } from '@/api'; export const JustAddedContactMsg: FC = () => { return ( You've successfully accepted the contact. - Until confirmed, it will be displayed as + Until confirmed, it will be displayed as{' '} +
    You can confirm it right now or return to this process later by using the "Show code" button.
    diff --git a/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx b/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx index 612c0d6..f45219c 100644 --- a/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx +++ b/src/components/ContactsList/_modals/ContactUpsertModal/ContactAddModal.tsx @@ -13,7 +13,7 @@ export const ContactAddModal: FC = ({ onSubmitted, onCance = ({ onSubmitted, onCan void; onCancel: () => void; - sucessMsg: string; + successMsg: string; errorMsg: string; modalTitle: string; modalSubtitle: string; @@ -27,7 +27,7 @@ export const ContactUpsertModal: FC = ({ onCancel, fields, errorMsg, - sucessMsg, + successMsg, modalTitle, modalSubtitle, disabledPaymailInput, @@ -38,7 +38,7 @@ export const ContactUpsertModal: FC = ({ const { name, paymail, phone, setName, setPaymail, setPhone } = fields; const onSuccess = async () => { - setSuccessMessage(sucessMsg); + setSuccessMessage(successMsg); await modalCloseTimeout(); onSubmitted(); };