Skip to content
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

feature: add contact book to the wallet #878

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions src/apps/popup/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import { TransferNftPage } from '@popup/pages/transfer-nft';
import { ChangePasswordPage } from '@popup/pages/change-password';
import { StakesPage } from '@popup/pages/stakes';
import { ErrorPath, WindowErrorPage } from '@layout/error';
import { ContactsBookPage } from '@popup/pages/contacts';
import { AddContactPage } from '@popup/pages/add-contact';
import { ContactDetailsPage } from '@popup/pages/contact-details';

export function AppRouter() {
const isLocked = useSelector(selectVaultIsLocked);
Expand Down Expand Up @@ -265,6 +268,12 @@ function AppRoutes() {
/>
}
/>
<Route path={RouterPath.ContactList} element={<ContactsBookPage />} />
<Route path={RouterPath.AddContact} element={<AddContactPage />} />
<Route
path={RouterPath.ContactDetails}
element={<ContactDetailsPage />}
/>
</Routes>
);
}
59 changes: 59 additions & 0 deletions src/apps/popup/pages/add-contact/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { FieldErrors, UseFormRegister } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import {
ContentContainer,
InputsContainer,
ParagraphContainer,
SpacingSize
} from '@libs/layout';
import {
FormField,
FormFieldStatus,
Input,
TextArea,
Typography
} from '@libs/ui';
import { ContactFromValues } from '@libs/ui/forms/contact';

interface AddContactPageContentProps {
register: UseFormRegister<ContactFromValues>;
errors: FieldErrors<ContactFromValues>;
}

export const AddContactPageContent = ({
register,
errors
}: AddContactPageContentProps) => {
const { t } = useTranslation();

return (
<ContentContainer>
<ParagraphContainer top={SpacingSize.XL}>
<Typography type="header">New contact</Typography>
</ParagraphContainer>
<InputsContainer>
<Input
type="text"
label={t('Name')}
{...register('name')}
placeholder={t('Name')}
error={!!errors.name}
validationText={errors.name?.message}
/>
<FormField
statusText={errors.publicKey?.message}
status={!!errors.publicKey ? FormFieldStatus.Error : undefined}
label={t('Public address')}
>
<TextArea
{...register('publicKey')}
placeholder={t('0x')}
type="captionHash"
/>
</FormField>
</InputsContainer>
</ContentContainer>
);
};
117 changes: 117 additions & 0 deletions src/apps/popup/pages/add-contact/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import {
FooterButtonsContainer,
HeaderSubmenuBarNavLink,
PopupHeader,
PopupLayout
} from '@libs/layout';
import { Button } from '@libs/ui';
import { AddContactPageContent } from '@popup/pages/add-contact/content';
import { ContactFromValues, useContactForm } from '@libs/ui/forms/contact';
import { selectAllContactsNames } from '@background/redux/contacts/selectors';
import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation';
import { dispatchToMainStore } from '@background/redux/utils';
import { newContactAdded } from '@background/redux/contacts/actions';
import { RouterPath, useTypedLocation, useTypedNavigate } from '@popup/router';
import { SuccessScreen } from '@popup/pages/add-contact/success-screen';

export const AddContactPage = () => {
const [showSuccessScreen, setShowSuccessScreen] = useState(false);

const { t } = useTranslation();
const navigate = useTypedNavigate();
const { state } = useTypedLocation();

const contactsNames = useSelector(selectAllContactsNames);

const {
register,
handleSubmit,
formState: { errors, isValid }
} = useContactForm(contactsNames, state?.recipientPublicKey);

const onSubmit = ({ name, publicKey }: ContactFromValues) => {
const lastModified = new Date().toISOString();

dispatchToMainStore(
newContactAdded({ name, publicKey, lastModified: lastModified })
).finally(() => {
setShowSuccessScreen(true);
});
};

useEffect(() => {
const keyDownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
!showSuccessScreen && handleSubmit(onSubmit)();
}
};

window.addEventListener('keydown', keyDownHandler);

return () => window.removeEventListener('keydown', keyDownHandler);
}, [handleSubmit, navigate, showSuccessScreen]);

const isButtonDisabled = calculateSubmitButtonDisabled({ isValid });
const needToRedirectToHome = Boolean(state?.recipientPublicKey);

return (
<PopupLayout
renderHeader={() => (
<PopupHeader
withNetworkSwitcher
withMenu
withConnectionStatus
renderSubmenuBarItems={() => (
<HeaderSubmenuBarNavLink
linkType={
showSuccessScreen || needToRedirectToHome ? 'close' : 'back'
}
onClick={() => {
if (needToRedirectToHome) {
navigate(RouterPath.Home);
return;
}
showSuccessScreen
? navigate(RouterPath.ContactList)
: navigate(-1);
}}
/>
)}
/>
)}
renderContent={() =>
showSuccessScreen ? (
<SuccessScreen needToRedirectToHome={needToRedirectToHome} />
) : (
<AddContactPageContent register={register} errors={errors} />
)
}
renderFooter={() => (
<FooterButtonsContainer>
{showSuccessScreen ? (
<Button
onClick={() =>
needToRedirectToHome
? navigate(RouterPath.Home)
: navigate(RouterPath.ContactList)
}
>
<Trans t={t}>Done</Trans>
</Button>
) : (
<Button
onClick={handleSubmit(onSubmit)}
disabled={isButtonDisabled}
>
<Trans t={t}>Add contact</Trans>
</Button>
)}
</FooterButtonsContainer>
)}
/>
);
};
58 changes: 58 additions & 0 deletions src/apps/popup/pages/add-contact/success-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useEffect } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import {
ContentContainer,
ParagraphContainer,
SpacingSize,
VerticalSpaceContainer
} from '@libs/layout';
import { SvgIcon, Typography } from '@libs/ui';
import { RouterPath, useTypedNavigate } from '@popup/router';

interface SuccessScreenProps {
needToRedirectToHome: boolean;
}

export const SuccessScreen = ({ needToRedirectToHome }: SuccessScreenProps) => {
const { t } = useTranslation();
const navigate = useTypedNavigate();

useEffect(() => {
const keyDownHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
needToRedirectToHome
? navigate(RouterPath.Home)
: navigate(RouterPath.ContactList);
}
};
window.addEventListener('keydown', keyDownHandler);

return () => window.removeEventListener('keydown', keyDownHandler);
}, [navigate, needToRedirectToHome]);

return (
<ContentContainer>
<ParagraphContainer top={SpacingSize.XL}>
<SvgIcon
src="assets/illustrations/success.svg"
width={200}
height={120}
/>
<VerticalSpaceContainer top={SpacingSize.XL}>
<Typography type="header">
<Trans t={t}>All done!</Trans>
</Typography>
</VerticalSpaceContainer>
<VerticalSpaceContainer top={SpacingSize.Medium}>
<Typography type="body" color="contentSecondary">
<Trans t={t}>
You will see this contact’s details and select it when you
transfer or delegate tokens.
</Trans>
</Typography>
</VerticalSpaceContainer>
</ParagraphContainer>
</ContentContainer>
);
};
6 changes: 2 additions & 4 deletions src/apps/popup/pages/backup-secret-phrase/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { Button } from '@libs/ui';

import { RouterPath, useTypedNavigate } from '@popup/router';
import { BackupSecretPhrasePasswordPage } from '@popup/pages/backup-secret-phrase-password';
import { PasswordProtectionPage } from '@popup/pages/password-protection-page';

import { BackupSecretPhrasePageContent } from './content';

Expand All @@ -27,9 +27,7 @@ export function BackupSecretPhrasePage() {

if (!isPasswordConfirmed) {
return (
<BackupSecretPhrasePasswordPage
setPasswordConfirmed={setPasswordConfirmed}
/>
<PasswordProtectionPage setPasswordConfirmed={setPasswordConfirmed} />
);
}

Expand Down
36 changes: 36 additions & 0 deletions src/apps/popup/pages/contact-details/deleting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';

import {
ContentContainer,
IllustrationContainer,
ParagraphContainer,
SpacingSize
} from '@libs/layout';
import { SvgIcon, Typography } from '@libs/ui';

export const DeleteContactPageContent = () => {
const { t } = useTranslation();

return (
<ContentContainer>
<IllustrationContainer>
<SvgIcon
src="assets/illustrations/remove-wallet.svg"
width={183}
height={120}
/>
</IllustrationContainer>
<ParagraphContainer top={SpacingSize.XL}>
<Typography type="header">
<Trans t={t}>Delete contact?</Trans>
</Typography>
</ParagraphContainer>
<ParagraphContainer top={SpacingSize.Medium}>
<Typography type="body" color="contentSecondary">
<Trans t={t}>You won’t be able to restore it.</Trans>
</Typography>
</ParagraphContainer>
</ContentContainer>
);
};
50 changes: 50 additions & 0 deletions src/apps/popup/pages/contact-details/details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import styled from 'styled-components';
import { Trans, useTranslation } from 'react-i18next';

import {
ContentContainer,
LeftAlignedFlexColumn,
SpacingSize
} from '@libs/layout';
import { Avatar, Hash, HashVariant, Tile, Typography } from '@libs/ui';
import { formatShortTimestamp } from '@libs/ui/utils/formatters';
import { Contact } from '@background/redux/contacts/types';

const Container = styled(LeftAlignedFlexColumn)`
margin-top: 24px;
padding: 32px 16px 16px;

gap: 32px;
`;

interface ContactDetailsProps {
contact: Contact;
}

export const ContactDetails = ({ contact }: ContactDetailsProps) => {
const { t } = useTranslation();

return (
<ContentContainer>
<Tile>
<Container>
<Avatar publicKey={contact.publicKey} size={89} />
<LeftAlignedFlexColumn gap={SpacingSize.Large}>
<Typography type="header">{contact.name}</Typography>
<Hash
value={contact.publicKey}
variant={HashVariant.CaptionHash}
color="contentPrimary"
/>
<Typography type="captionRegular" color="contentSecondary">
<Trans t={t}>
Last edited: {formatShortTimestamp(contact.lastModified)}
</Trans>
</Typography>
</LeftAlignedFlexColumn>
</Container>
</Tile>
</ContentContainer>
);
};
Loading
Loading