Skip to content

Commit

Permalink
Merge pull request #1093 from Koniverse/issue-1066
Browse files Browse the repository at this point in the history
[issue-1066]: Swipe to delete/copy account
  • Loading branch information
nguyenduythuc authored Oct 26, 2023
2 parents 3473bbe + 62a8b02 commit 1abc068
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 22 deletions.
1 change: 0 additions & 1 deletion src/components/common/SelectAccountItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export const SelectAccountItem = ({
paddingLeft: 12,
paddingRight: 4,
backgroundColor: theme.colorBgSecondary,
marginBottom: theme.marginXS,
borderRadius: theme.borderRadiusLG,
}}
onPress={() => onSelectAccount && onSelectAccount(address)}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/design-system-ui/button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const Button: React.FC<ButtonProps> = props => {
onPressOut={_onPressOut}
onShowUnderlay={_onShowUnderlay}
onHideUnderlay={_onHideUnderlay}>
<View style={[_style.container, { maxWidth: '100%', paddingHorizontal: theme.padding - 4 }]}>
<View style={[_style.container, { maxWidth: '100%', paddingHorizontal: children ? theme.padding - 4 : 0 }]}>
{iconNode}
{typeof children === 'string' ? (
<Text numberOfLines={1} style={[textStyle]}>
Expand Down
277 changes: 257 additions & 20 deletions src/screens/Account/AccountsScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import { AccountJson } from '@subwallet/extension-base/background/types';
import { SelectAccountItem } from 'components/common/SelectAccountItem';
import React, { useCallback, useMemo, useRef } from 'react';
import { Keyboard, ListRenderItemInfo, View } from 'react-native';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Keyboard, ListRenderItemInfo, Share, StyleProp, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { RootState } from 'stores/index';
import { FileArrowDown, MagnifyingGlass, PlusCircle, Swatches } from 'phosphor-react-native';
import {
Copy,
CopySimple,
FileArrowDown,
MagnifyingGlass,
PlusCircle,
QrCode,
Share as ShareIcon,
Swatches,
Trash,
} from 'phosphor-react-native';
import { AccountsScreenProps, RootNavigationProps } from 'routes/index';
import i18n from 'utils/i18n/i18n';
import { MarginBottomForSubmitButton } from 'styles/sharedStyles';
import { saveCurrentAccountAddress } from 'messaging/index';
import { FontMedium, FontSemiBold, MarginBottomForSubmitButton, STATUS_BAR_HEIGHT } from 'styles/sharedStyles';
import { forgetAccount, saveCurrentAccountAddress } from 'messaging/index';
import { isAccountAll } from '@subwallet/extension-base/utils';
import { findAccountByAddress } from 'utils/index';
import { findAccountByAddress, toShort } from 'utils/index';
import { CurrentAccountInfo } from '@subwallet/extension-base/background/KoniTypes';
import { Button, Icon } from 'components/design-system-ui';
import { Avatar, Button, Icon, QRCode, SwModal, Typography } from 'components/design-system-ui';
import { AccountCreationArea } from 'components/common/Account/AccountCreationArea';
import { FlatListScreen } from 'components/FlatListScreen';
import { EmptyList } from 'components/EmptyList';
import { ModalRef } from 'types/modalRef';
import { Swipeable } from 'react-native-gesture-handler';
import { useSubWalletTheme } from 'hooks/useSubWalletTheme';
import Clipboard from '@react-native-clipboard/clipboard';
import Toast, { useToast } from 'react-native-toast-notifications';
import { deviceHeight, TOAST_DURATION } from 'constants/index';
import { ColorMap } from 'styles/color';
import ToastContainer from 'react-native-toast-notifications';
import { isEthereumAddress } from '@polkadot/util-crypto';
import DeleteModal from 'components/common/Modal/DeleteModal';
import useConfirmModal from 'hooks/modal/useConfirmModal';
import useGoHome from 'hooks/screen/useGoHome';

const renderListEmptyComponent = () => {
return (
Expand All @@ -36,15 +57,29 @@ const searchFunction = (items: AccountJson[], searchString: string) => {
account.address.toLowerCase().includes(searchString.toLowerCase()),
);
};
const OFFSET_BOTTOM = deviceHeight - STATUS_BAR_HEIGHT - 140;

const receiveModalContentWrapper: StyleProp<any> = {
alignItems: 'center',
width: '100%',
};

export const AccountsScreen = ({
route: {
params: { pathName },
},
}: AccountsScreenProps) => {
const toast = useToast();
const theme = useSubWalletTheme().swThemes;
const goHome = useGoHome();
const navigation = useNavigation<RootNavigationProps>();
const fullAccounts = useSelector((state: RootState) => state.accountState.accounts);
const currentAccountAddress = useSelector((state: RootState) => state.accountState.currentAccount?.address);
let svg: { toDataURL: (arg0: (data: any) => void) => void };
const [qrModalVisible, setQrModalVisible] = useState<boolean>(false);
const [selectedAddress, setSelectedAddress] = useState<string>('');
const [deleting, setDeleting] = useState(false);
const toastRef = useRef<ToastContainer>(null);
const accounts = useMemo(() => {
if (fullAccounts.length > 2) {
return fullAccounts;
Expand All @@ -56,6 +91,47 @@ export const AccountsScreen = ({
const createAccountRef = useRef<ModalRef>();
const importAccountRef = useRef<ModalRef>();
const attachAccountRef = useRef<ModalRef>();
let row = useRef<(Swipeable | null)[]>([]);
let prevOpenedRow = useRef<Swipeable>(null);
const onDelete = useCallback(() => {
if (selectedAddress) {
setDeleting(true);
forgetAccount(selectedAddress)
.then(() => {
goHome();
})
.catch((e: Error) => {
toast.show(e.message, { type: 'danger' });
})
.finally(() => {
setDeleting(false);
});
}
}, [selectedAddress, goHome, toast]);

const closeOpenedRow = (index: number) => {
if (prevOpenedRow && prevOpenedRow.current !== row.current?.[index]) {
prevOpenedRow.current?.close();
}
prevOpenedRow = { current: row.current?.[index] };
};

const {
onPress: onPressDelete,
onCancelModal: onCancelDelete,
visible: deleteVisible,
onCompleteModal: onCompleteDeleteModal,
setVisible: setDeleteVisible,
} = useConfirmModal(onDelete);

const copyToClipboard = useCallback(
(text: string) => {
toast.hideAll();
toast.show(i18n.common.copiedToClipboard);
Clipboard.setString(text);
},
[toast],
);

const selectAccount = useCallback(
(accAddress: string) => {
Expand Down Expand Up @@ -90,25 +166,98 @@ export const AccountsScreen = ({
[currentAccountAddress, pathName, accounts, navigation],
);

const onShareImg = () => {
svg.toDataURL(data => {
const shareImageBase64 = {
title: 'QR',
message: `My Public Address to Receive ${selectedAddress}`,
url: `data:image/png;base64,${data}`,
};
Share.share(shareImageBase64);
});
};

const rightSwipeActions = useCallback(
(address: string, index: number) => {
return () => (
<View
style={{
flexDirection: 'row',
height: '100%',
alignItems: 'center',
paddingRight: theme.padding,
gap: theme.paddingXS,
}}>
<Button
shape={'circle'}
style={{ backgroundColor: 'rgba(217, 163, 62, 0.1)' }}
type={'ghost'}
icon={<Icon phosphorIcon={Copy} size={'sm'} iconColor={theme['gold-6']} />}
size={'xs'}
onPress={() => {
copyToClipboard(address);
row.current?.[index]?.close();
}}
/>
<Button
shape={'circle'}
style={{ backgroundColor: 'rgba(0, 75, 255, 0.1)' }}
type={'ghost'}
icon={<Icon phosphorIcon={QrCode} size={'sm'} iconColor={theme.colorPrimary} />}
size={'xs'}
onPress={() => {
row.current?.[index]?.close();
setSelectedAddress(address);
setQrModalVisible(true);
}}
/>
<Button
shape={'circle'}
style={{ backgroundColor: 'rgba(191, 22, 22, 0.1)' }}
type={'ghost'}
icon={<Icon phosphorIcon={Trash} size={'sm'} iconColor={theme.colorError} />}
size={'xs'}
onPress={() => {
row.current?.[index]?.close();
setSelectedAddress(address);
onPressDelete();
}}
loading={deleting}
/>
</View>
);
},
[copyToClipboard, deleting, onPressDelete, row, theme],
);

const renderItem = useCallback(
({ item }: ListRenderItemInfo<AccountJson>) => {
({ item, index }: ListRenderItemInfo<AccountJson>) => {
const isAllAccount = isAccountAll(item.address);

return (
<SelectAccountItem
key={item.address}
address={item.address}
accountName={item.name}
isSelected={currentAccountAddress === item.address}
isAllAccount={isAllAccount}
onSelectAccount={selectAccount}
onPressDetailBtn={() => {
navigation.navigate('EditAccount', { address: item.address, name: item.name || '' });
}}
/>
<Swipeable
enabled={!isAllAccount}
ref={ref => (row.current[index] = ref)}
friction={2}
leftThreshold={80}
rightThreshold={40}
onSwipeableWillOpen={() => closeOpenedRow(index)}
renderRightActions={rightSwipeActions(item.address, index)}>
<SelectAccountItem
key={item.address}
address={item.address}
accountName={item.name}
isSelected={currentAccountAddress === item.address}
isAllAccount={isAllAccount}
onSelectAccount={selectAccount}
onPressDetailBtn={() => {
navigation.navigate('EditAccount', { address: item.address, name: item.name || '' });
}}
/>
</Swipeable>
);
},
[currentAccountAddress, navigation, selectAccount],
[currentAccountAddress, navigation, rightSwipeActions, selectAccount],
);

const onPressFooterBtn = (action: () => void) => {
Expand Down Expand Up @@ -155,6 +304,7 @@ export const AccountsScreen = ({
onPressBack={() => navigation.goBack()}
title={i18n.header.selectAccount}
items={accounts}
flatListStyle={{ gap: theme.paddingXS }}
renderItem={renderItem}
renderListEmptyComponent={renderListEmptyComponent}
searchFunction={searchFunction}
Expand All @@ -169,6 +319,93 @@ export const AccountsScreen = ({
attachAccountRef={attachAccountRef}
allowToShowSelectType={true}
/>

<SwModal isUseModalV2 modalVisible={qrModalVisible} setVisible={setQrModalVisible}>
<View style={receiveModalContentWrapper}>
<Typography.Text
size={'lg'}
style={{
color: theme.colorWhite,
...FontSemiBold,
}}>
{i18n.header.yourAddress}
</Typography.Text>
<View style={{ paddingTop: 38 }}>
{selectedAddress && <QRCode qrRef={(ref?) => (svg = ref)} value={selectedAddress} errorLevel={'Q'} />}
</View>

<View
style={{
height: 48,
flexDirection: 'row',
backgroundColor: theme.colorBgSecondary,
padding: theme.paddingXXS,
paddingLeft: theme.paddingSM,
alignItems: 'center',
gap: theme.paddingXS,
borderRadius: theme.borderRadiusLG,
marginVertical: theme.margin,
}}>
<Avatar
value={selectedAddress}
size={24}
theme={isEthereumAddress(selectedAddress) ? 'ethereum' : 'polkadot'}
/>

<Typography.Text
style={{
color: theme.colorTextLight4,
...FontMedium,
}}>
{toShort(selectedAddress, 7, 7)}
</Typography.Text>

<Button
icon={<Icon phosphorIcon={CopySimple} weight={'bold'} size={'sm'} iconColor={theme.colorTextLight4} />}
type={'ghost'}
size={'xs'}
onPress={() => copyToClipboard(selectedAddress)}
/>
</View>

<View
style={{
marginHorizontal: -theme.size,
paddingHorizontal: theme.size,
gap: theme.size,
flexDirection: 'row',
paddingTop: theme.size,
borderTopColor: theme.colorBgSecondary,
borderTopWidth: 2,
borderStyle: 'solid',
}}>
<Button
style={{ flex: 1 }}
icon={<Icon phosphorIcon={ShareIcon} weight={'fill'} size={'lg'} />}
onPress={onShareImg}>
{i18n.common.share}
</Button>
</View>
{
<Toast
duration={TOAST_DURATION}
normalColor={ColorMap.notification}
ref={toastRef}
placement={'bottom'}
offsetBottom={OFFSET_BOTTOM}
/>
}
</View>
</SwModal>

<DeleteModal
title={i18n.header.removeThisAcc}
visible={deleteVisible}
message={i18n.removeAccount.removeAccountMessage}
onCancelModal={onCancelDelete}
onCompleteModal={onCompleteDeleteModal}
setVisible={setDeleteVisible}
/>
</>
);
};

0 comments on commit 1abc068

Please sign in to comment.