Skip to content

Commit

Permalink
Merge pull request #76 from bitcoin-sv/feat-finalize-contacts
Browse files Browse the repository at this point in the history
feat(SPV-679): finalize contacts
  • Loading branch information
chris-4chain authored May 8, 2024
2 parents 97118ee + 89088e7 commit e01eb9b
Show file tree
Hide file tree
Showing 31 changed files with 572 additions and 230 deletions.
84 changes: 31 additions & 53 deletions src/api/requests/contact.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,50 @@
/* 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) => {
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, 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 = 'not-confirmed';
}
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: string) => {
await axios.patch(`/contact/confirmed`, {
passcode: totp,
contact,
});
};

/// Mocked contacts

let contacts: Contact[] = [
{
paymail: '[email protected]',
name: 'Bob',
status: 'confirmed',
},
{
paymail: '[email protected]',
name: 'Tester',
status: 'awaiting-acceptance',
},
{
paymail: '[email protected]',
name: 'The Guy',
status: 'not-confirmed',
},
];
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);
};
23 changes: 18 additions & 5 deletions src/api/types/contact.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
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 = {
created_at: string;
updated_at: string;
deleted_at: string;
metadata?: ContactMetadata;
id: string;
pubKey: string;
paymail: string;
name: string;
status: ContactStatus;
fullName: string;
};

//TODO: Add more fields (...metadata)
export type ContactMetadata = {
phoneNumber?: string;
};
74 changes: 42 additions & 32 deletions src/components/ContactsList/ContactsTable.tsx/ContactsTable.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,13 +12,14 @@ 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';
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();
Expand Down Expand Up @@ -57,37 +58,46 @@ export const ContactsTable: FC = () => {
</tr>
</thead>
<tbody>
{sortedContacts.map(({ paymail, status, name }) => (
<tr key={paymail} style={{ height: 50 }}>
<LargeTd>{paymail}</LargeTd>
<MediumTd>{name}</MediumTd>
<MediumTd>
<StatusBadge status={status} />
</MediumTd>
<MediumTd>
{status !== ContactAwaitingAcceptance ? (
<SmallButton
variant="accept"
onClick={() => {
openVerificationWindow(paymail);
}}
>
Show code
</SmallButton>
) : (
<AcceptReject
{sortedContacts.map((contact) => {
const { paymail, fullName, status } = contact;
return (
<tr key={paymail} style={{ height: 50 }}>
<LargeTd>
<TextWrapper>{paymail}</TextWrapper>
</LargeTd>
<MediumTd>{fullName}</MediumTd>
<MediumTd>
<StatusBadge status={status} />
</MediumTd>
<MediumTd>
{status !== ContactAwaitingAcceptance && <ContactEdit contact={contact} />}
{status !== ContactAwaitingAcceptance ? (
<SmallButton
variant="accept"
onClick={() => {
openVerificationWindow(paymail);
}}
>
Show code
</SmallButton>
) : (
<AcceptReject
paymail={paymail}
onAccept={() => {
openVerificationWindow(paymail, true);
refresh();
}}
onReject={refresh}
/>
)}
<SetPaymailButton
paymail={paymail}
onAccept={() => {
openVerificationWindow(paymail, true);
refresh();
}}
onReject={refresh}
variant={status === ContactConfirmed ? 'accept' : 'primary'}
/>
)}
<SetPaymailButton paymail={paymail} variant={status === ContactConfirmed ? 'accept' : 'primary'} />
</MediumTd>
</tr>
))}
</MediumTd>
</tr>
);
})}
</tbody>
</Table>
)}
Expand All @@ -101,7 +111,7 @@ export const ContactsTable: FC = () => {
setJustAddedContact(false);
}}
>
{justAddedContact && contactForVerification.status === 'not-confirmed' && <JustAddedContactMsg />}
{justAddedContact && contactForVerification.status === ContactNotConfirmed && <JustAddedContactMsg />}
</VerifyModal>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ 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 (
<Container>
<SuccessInfo>You've successfully accepted the contact.</SuccessInfo>
Until confirmed, it will be displayed as <StatusBadge status="not-confirmed" />. <br />
Until confirmed, it will be displayed as{' '}
<StatusBadge status={ContactNotConfirmed} style={{ display: 'inline' }} />
<br />
You can confirm it right now or return to this process later by using the "Show code" button.
</Container>
);
};

const Container = styled.div`
padding: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
`;

const SuccessInfo = styled.div`
Expand Down
31 changes: 25 additions & 6 deletions src/components/ContactsList/ContactsTable.tsx/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
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';
import styled from '@emotion/styled';

type StatusBadgeProps = {
status: ContactStatus;
status: ContactStatus | 'unknown';
};

export const StatusBadge: FC<StatusBadgeProps> = ({ status }) => {
const { label, color } = contactStatuses[status];
export const StatusBadge: FC<StatusBadgeProps & React.HTMLAttributes<HTMLDivElement>> = ({ status, ...props }) => {
const { label, color } = contactStatuses[status] ?? { label: 'Unknown', color: 'error' };

return <Chip size="small" label={label} color={color} />;
return (
<ChipContainer {...props}>
<Chip size="small" label={label} color={color} />
</ChipContainer>
);
};

const contactStatuses: Record<ContactStatus, { label: string; color: ChipProps['color'] }> = {
const contactStatuses: Record<ContactStatus | 'unknown', { label: string; color: ChipProps['color'] }> = {
[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;
}
`;
4 changes: 2 additions & 2 deletions src/components/ContactsList/_modals/ContactAdd.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -17,7 +17,7 @@ export const ContactAdd: FC = () => {
<Button variant="primary" small onClick={() => setOpen(true)}>
Add contact
</Button>
{open && <ContactAddModal open={true} onSubmitted={onSubmitted} onCancel={() => setOpen(false)} />}
{open && <ContactAddModal onSubmitted={onSubmitted} onCancel={() => setOpen(false)} />}
</>
);
};
Loading

0 comments on commit e01eb9b

Please sign in to comment.