From d5430382afbb22066aa87455a95b1853ebb09291 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 26 Jan 2024 10:08:14 +0200 Subject: [PATCH] [ENG-3370] feat: improve get address screen (#715) * init getAddress screen revamp * update ui * update btc address select request * update stx screen and add btcAddress to stacks connect response * ui fixes * minor fixes * disable account actions on connection requests * added icon borderRadius * remove debug log * Fix typing between sats connect and extension for providers (#768) * Fix typing between sats connect and extension for providers * update sats-connect version --------- Co-authored-by: Mahmoud Aboelenein --------- Co-authored-by: Victor Kirov --- package-lock.json | 14 +- package.json | 2 +- src/app/hooks/useBtcAddressRequest.ts | 31 +- src/app/routes/index.tsx | 6 +- src/app/screens/accountList/index.tsx | 36 ++- .../btcSelectAddressScreen/accountView.tsx | 114 ------- .../screens/btcSelectAddressScreen/index.tsx | 306 ------------------ src/app/screens/connect/addressPurposeBox.tsx | 80 +++++ .../authenticationRequest/index.tsx | 168 ++++++---- .../connect/btcSelectAddressScreen/helper.ts | 48 +++ .../connect/btcSelectAddressScreen/index.tsx | 212 ++++++++++++ src/app/screens/connect/permissionsList.tsx | 47 +++ src/app/screens/connect/selectAccount.tsx | 100 ++++++ .../img/webInteractions/authPlaceholder.svg | 6 - src/inpage/index.ts | 9 +- src/locales/en.json | 30 +- 16 files changed, 679 insertions(+), 530 deletions(-) delete mode 100644 src/app/screens/btcSelectAddressScreen/accountView.tsx delete mode 100644 src/app/screens/btcSelectAddressScreen/index.tsx create mode 100644 src/app/screens/connect/addressPurposeBox.tsx rename src/app/screens/{ => connect}/authenticationRequest/index.tsx (65%) create mode 100644 src/app/screens/connect/btcSelectAddressScreen/helper.ts create mode 100644 src/app/screens/connect/btcSelectAddressScreen/index.tsx create mode 100644 src/app/screens/connect/permissionsList.tsx create mode 100644 src/app/screens/connect/selectAccount.tsx delete mode 100644 src/assets/img/webInteractions/authPlaceholder.svg diff --git a/package-lock.json b/package-lock.json index c1fcce149..dfc58227c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "redux": "^4.0.5", "redux-persist": "^6.0.0", "redux-state-sync": "^3.1.4", - "sats-connect": "1.3.0", + "sats-connect": "1.4.0", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", @@ -11958,9 +11958,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sats-connect": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-1.3.0.tgz", - "integrity": "sha512-qKWfqmvJDyPaDCawMa4wi7L+S1t8AfgzDn7835VAuqNlhb3RC+uME5kRuqItwyfHgFY6O8kJinQjWLhmV9PsmA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-1.4.0.tgz", + "integrity": "sha512-jXc9sFdFp9iDDXF4ptSiiWEjHiJUyhvPh2h37QI6IrfhH00Awxjqjhq8PXmfum2ODMzYKDEzI1gXo/UUaToCCA==", "dependencies": { "jsontokens": "^4.0.1", "process": "^0.11.10", @@ -23205,9 +23205,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sats-connect": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-1.3.0.tgz", - "integrity": "sha512-qKWfqmvJDyPaDCawMa4wi7L+S1t8AfgzDn7835VAuqNlhb3RC+uME5kRuqItwyfHgFY6O8kJinQjWLhmV9PsmA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-1.4.0.tgz", + "integrity": "sha512-jXc9sFdFp9iDDXF4ptSiiWEjHiJUyhvPh2h37QI6IrfhH00Awxjqjhq8PXmfum2ODMzYKDEzI1gXo/UUaToCCA==", "requires": { "jsontokens": "^4.0.1", "process": "^0.11.10", diff --git a/package.json b/package.json index 910e00b9e..6de446d98 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "redux": "^4.0.5", "redux-persist": "^6.0.0", "redux-state-sync": "^3.1.4", - "sats-connect": "1.3.0", + "sats-connect": "1.4.0", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", diff --git a/src/app/hooks/useBtcAddressRequest.ts b/src/app/hooks/useBtcAddressRequest.ts index 5207b12fe..eae5089e7 100644 --- a/src/app/hooks/useBtcAddressRequest.ts +++ b/src/app/hooks/useBtcAddressRequest.ts @@ -2,15 +2,30 @@ import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types import useWalletSelector from '@hooks/useWalletSelector'; import { decodeToken } from 'jsontokens'; import { useLocation } from 'react-router-dom'; -import { Address, AddressPurpose, GetAddressOptions, GetAddressResponse } from 'sats-connect'; +import { + Address, + AddressPurpose, + AddressType, + GetAddressOptions, + GetAddressResponse, +} from 'sats-connect'; const useBtcAddressRequest = () => { - const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useWalletSelector(); + const { + btcAddress, + ordinalsAddress, + btcPublicKey, + ordinalsPublicKey, + stxAddress, + stxPublicKey, + selectedAccount, + } = useWalletSelector(); const { search } = useLocation(); const params = new URLSearchParams(search); const requestToken = params.get('addressRequest') ?? ''; const request = decodeToken(requestToken) as any as GetAddressOptions; const tabId = params.get('tabId') ?? '0'; + const origin = params.get('origin') ?? ''; const approveBtcAddressRequest = () => { const addressesResponse: Address[] = request.payload.purposes.map((purpose: AddressPurpose) => { @@ -19,12 +34,23 @@ const useBtcAddressRequest = () => { address: ordinalsAddress, publicKey: ordinalsPublicKey, purpose: AddressPurpose.Ordinals, + addressType: AddressType.p2tr, + }; + } + if (purpose === AddressPurpose.Stacks) { + return { + address: stxAddress, + publicKey: stxPublicKey, + purpose: AddressPurpose.Stacks, + addressType: AddressType.stacks, }; } return { address: btcAddress, publicKey: btcPublicKey, purpose: AddressPurpose.Payment, + addressType: + selectedAccount?.accountType === 'ledger' ? AddressType.p2wpkh : AddressType.p2sh, }; }); const response: GetAddressResponse = { @@ -50,6 +76,7 @@ const useBtcAddressRequest = () => { return { payload: request.payload, tabId, + origin, requestToken, approveBtcAddressRequest, cancelAddressRequest, diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 1fc350bd0..93d88710d 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -4,10 +4,8 @@ import OnboardingGuard from '@components/guards/onboarding'; import { SingleTabGuard } from '@components/guards/singleTab'; import ScreenContainer from '@components/screenContainer'; import AccountList from '@screens/accountList'; -import AuthenticationRequest from '@screens/authenticationRequest'; import BackupWallet from '@screens/backupWallet'; import BackupWalletSteps from '@screens/backupWalletSteps'; -import BtcSelectAddressScreen from '@screens/btcSelectAddressScreen'; import BtcSendScreen from '@screens/btcSendScreen'; import Buy from '@screens/buy'; import CoinDashboard from '@screens/coinDashboard'; @@ -18,6 +16,8 @@ import ConfirmInscriptionRequest from '@screens/confirmInscriptionRequest'; import ConfirmNftTransaction from '@screens/confirmNftTransaction'; import ConfirmOrdinalTransaction from '@screens/confirmOrdinalTransaction'; import ConfirmStxTransaction from '@screens/confirmStxTransaction'; +import AuthenticationRequest from '@screens/connect/authenticationRequest'; +import BtcSelectAddressScreen from '@screens/connect/btcSelectAddressScreen'; import CreateInscription from '@screens/createInscription'; import CreatePassword from '@screens/createPassword'; import CreateWalletSuccess from '@screens/createWalletSuccess'; @@ -61,9 +61,9 @@ import ChangePasswordScreen from '@screens/settings/changePassword'; import FiatCurrencyScreen from '@screens/settings/fiatCurrency'; import LockCountdown from '@screens/settings/lockCountdown'; import PrivacyPreferencesScreen from '@screens/settings/privacyPreferences'; -import SignatureRequest from '@screens/signatureRequest'; import SignBatchPsbtRequest from '@screens/signBatchPsbtRequest'; import SignPsbtRequest from '@screens/signPsbtRequest'; +import SignatureRequest from '@screens/signatureRequest'; import SpeedUpTransactionScreen from '@screens/speedUpTransaction'; import Stacking from '@screens/stacking'; import SwapScreen from '@screens/swap'; diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index d06dbe076..374ebd690 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -12,7 +12,7 @@ import { Plus } from '@phosphor-icons/react'; import { Account } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; export const Container = styled.div({ @@ -57,10 +57,14 @@ const Title = styled.div((props) => ({ function AccountList(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'ACCOUNT_SCREEN' }); const navigate = useNavigate(); + const { search } = useLocation(); + const params = new URLSearchParams(search); const { network, accountsList, selectedAccount, ledgerAccountsList } = useWalletSelector(); const { createAccount, switchAccount } = useWalletReducer(); const { enqueueFetchBalances } = useAccountBalance(); + const hideListActions = Boolean(params.get('hideListActions')) || false; + const displayedAccountsList = useMemo(() => { const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); return [...networkLedgerAccounts, ...accountsList]; @@ -112,20 +116,22 @@ function AccountList(): JSX.Element { ))} - - } - onPress={onCreateAccount} - text={t('NEW_ACCOUNT')} - transparent - /> - } - onPress={onImportLedgerAccount} - text={t('LEDGER_ACCOUNT')} - transparent - /> - + {!hideListActions ? ( + + } + onPress={onCreateAccount} + text={t('NEW_ACCOUNT')} + transparent + /> + } + onPress={onImportLedgerAccount} + text={t('LEDGER_ACCOUNT')} + transparent + /> + + ) : null} ); } diff --git a/src/app/screens/btcSelectAddressScreen/accountView.tsx b/src/app/screens/btcSelectAddressScreen/accountView.tsx deleted file mode 100644 index d4ca8a77d..000000000 --- a/src/app/screens/btcSelectAddressScreen/accountView.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; -import { Account } from '@secretkeylabs/xverse-core'; -import { getAccountGradient } from '@utils/gradient'; -import { getTruncatedAddress } from '@utils/helper'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -interface GradientCircleProps { - firstGradient: string; - secondGradient: string; - thirdGradient: string; -} -const GradientCircle = styled.div((props) => ({ - height: 40, - width: 40, - borderRadius: 25, - marginRight: 9, - background: `linear-gradient(to bottom,${props.firstGradient}, ${props.secondGradient},${props.thirdGradient} )`, -})); - -const Container = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const ColumnContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const AddressContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', -}); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', -}); - -const CurrentSelectedAccountText = styled.h1((props) => ({ - ...props.theme.body_bold_m, - color: props.theme.colors.white_0, - textAlign: 'start', -})); - -const AddressText = styled.h1((props) => ({ - ...props.theme.body_m, - marginTop: props.theme.spacing(1), - color: props.theme.colors.white_400, -})); - -const BitcoinDot = styled.div((props) => ({ - borderRadius: 20, - background: props.theme.colors.feedback.caution, - width: 10, - marginRight: 4, - marginLeft: 4, - height: 10, -})); - -const OrdinalImage = styled.img({ - width: 12, - height: 12, - marginRight: 4, -}); - -interface Props { - account: Account; - isBitcoinTx: boolean; -} -function AccountView({ account, isBitcoinTx }: Props) { - const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); - const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); - - function getName() { - return ( - account?.accountName ?? - account?.bnsName ?? - `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}` - ); - } - - return ( - - - - - {getName()} - - - - {`${getTruncatedAddress(account?.ordinalsAddress)} / `} - - - - {`${getTruncatedAddress(account?.btcAddress)}`} - - - - - ); -} - -export default AccountView; diff --git a/src/app/screens/btcSelectAddressScreen/index.tsx b/src/app/screens/btcSelectAddressScreen/index.tsx deleted file mode 100644 index 5eb6b86a3..000000000 --- a/src/app/screens/btcSelectAddressScreen/index.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; -import XverseLogo from '@assets/img/settings/logo.svg'; -import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; -import { filterLedgerAccounts } from '@common/utils/ledger'; -import AccountRow from '@components/accountRow'; -import ActionButton from '@components/button'; -import Separator from '@components/separator'; -import useBtcAddressRequest from '@hooks/useBtcAddressRequest'; -import useWalletReducer from '@hooks/useWalletReducer'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { animated, useSpring } from '@react-spring/web'; -import { Account } from '@secretkeylabs/xverse-core'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { AddressPurpose } from 'sats-connect'; -import styled from 'styled-components'; -import AccountView from './accountView'; - -const TitleContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', - marginLeft: 30, - marginRight: 30, -}); - -const DropDownContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - flex: 1, - height: '100%', - alignItems: 'center', - justifyContent: 'flex-end', -}); - -const Container = styled.div({ - display: 'flex', - alignItems: 'center', -}); - -const LogoContainer = styled.div((props) => ({ - padding: props.theme.spacing(11), - marginBottom: props.theme.spacing(16), - borderBottom: `1px solid ${props.theme.colors.elevation3}`, -})); - -const AddressContainer = styled.div((props) => ({ - background: props.theme.colors.elevation2, - borderRadius: 40, - height: 24, - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: '3px 10px 3px 6px', - marginTop: props.theme.spacing(4), - marginRight: props.theme.spacing(2), -})); - -const AccountListContainer = styled(animated.div)((props) => ({ - paddingBottom: 20, - width: '100%', - borderRadius: 12, - height: 214, - marginTop: props.theme.spacing(9.5), - boxShadow: '0px 8px 104px rgba(0, 0, 0, 0.5)', - background: props.theme.colors.elevation2, - '&::-webkit-scrollbar': { - display: 'none', - }, - overflowY: 'auto', -})); - -const TopImage = styled.img({ - aspectRatio: 1, - height: 88, - borderWidth: 10, - borderColor: 'white', -}); - -const FunctionTitle = styled.h1((props) => ({ - ...props.theme.body_bold_l, - color: props.theme.colors.white_0, - marginTop: 16, -})); - -const AccountContainer = styled.button((props) => ({ - background: props.theme.colors.elevation1, - border: `1px solid ${props.theme.colors.elevation3}`, - borderRadius: 8, - width: '100%', - padding: '12px 16px', - display: 'flex', - flexDirection: 'row', - marginTop: props.theme.spacing(4), - ':hover': { - background: props.theme.colors.elevation2, - }, -})); - -const AccountText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - marginTop: 24, -})); - -const DappTitle = styled.h2((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_200, - marginTop: 12, - textAlign: 'center', -})); - -const AddressTextTitle = styled.h1((props) => ({ - ...props.theme.body_medium_l, - color: props.theme.colors.white_0, - fontSize: 10, - textAlign: 'center', -})); - -const OuterContainer = styled(animated.div)({ - display: 'flex', - flexDirection: 'column', - marginLeft: 16, - marginRight: 16, -}); - -const ButtonsContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-end', - marginBottom: props.theme.spacing(20), - marginTop: 82, -})); - -const BitcoinDot = styled.div((props) => ({ - borderRadius: 20, - background: props.theme.colors.feedback.caution, - width: 6, - marginRight: props.theme.spacing(3), - height: 6, -})); - -const AccountListRow = styled.div((props) => ({ - paddingTop: props.theme.spacing(8), - paddingLeft: 16, - paddingRight: 16, - ':hover': { - background: props.theme.colors.elevation3, - }, -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), - width: '100%', -})); - -const OrdinalImage = styled.img({ - width: 12, - height: 12, - marginRight: 8, -}); - -function BtcSelectAddressScreen() { - const [loading, setLoading] = useState(false); - const [showAccountList, setShowAccountList] = useState(false); - const navigate = useNavigate(); - const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); - const { selectedAccount, accountsList, ledgerAccountsList, network } = useWalletSelector(); - const { switchAccount } = useWalletReducer(); - const { payload, approveBtcAddressRequest, cancelAddressRequest } = useBtcAddressRequest(); - const springProps = useSpring({ - transform: showAccountList ? 'translateY(0%)' : 'translateY(100%)', - opacity: showAccountList ? 1 : 0, - config: { - tension: 160, - friction: 25, - }, - }); - const styles = useSpring({ - from: { - opacity: 0, - y: 24, - }, - to: { - y: 0, - opacity: 1, - }, - }); - - const confirmCallback = async () => { - setLoading(true); - approveBtcAddressRequest(); - window.close(); - }; - - const cancelCallback = () => { - cancelAddressRequest(); - window.close(); - }; - - const onChangeAccount = () => { - setShowAccountList(true); - }; - - const isAccountSelected = (account: Account) => - account.id === selectedAccount?.id && account.accountType === selectedAccount?.accountType; - - const handleAccountSelect = async (account: Account) => { - await switchAccount(account); - setShowAccountList(false); - }; - - const switchAccountBasedOnRequest = () => { - if (payload.network.type !== network.type) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('NETWORK_MISMATCH_ERROR_TITLE'), - error: t('NETWORK_MISMATCH_ERROR_DESCRIPTION'), - browserTx: true, - }, - }); - } - }; - - useEffect(() => { - switchAccountBasedOnRequest(); - }, []); - - const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); - - return ( - <> - - xverse logo - - - - - {t('TITLE')} - - {payload.purposes.map((purpose) => - purpose === AddressPurpose.Payment ? ( - - - {t('BITCOIN_ADDRESS')} - - ) : ( - - - {t('ORDINAL_ADDRESS')} - - ), - )} - - {payload.message} - - {showAccountList ? ( - - {[...networkLedgerAccounts, ...accountsList].map((account) => ( - - - - - ))} - - ) : ( - <> - {t('ACCOUNT')} - - - - Drop Down - - - - - - - - - - )} - - - ); -} - -export default BtcSelectAddressScreen; diff --git a/src/app/screens/connect/addressPurposeBox.tsx b/src/app/screens/connect/addressPurposeBox.tsx new file mode 100644 index 000000000..783d46021 --- /dev/null +++ b/src/app/screens/connect/addressPurposeBox.tsx @@ -0,0 +1,80 @@ +import { getTruncatedAddress } from '@utils/helper'; +import { AddressPurpose } from 'sats-connect'; +import styled from 'styled-components'; + +const AddressBox = styled.div((props) => ({ + padding: `${props.theme.spacing(10)}px ${props.theme.space.m}`, + display: 'flex', + maxHeight: 60, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: props.theme.colors.elevation6_800, + marginBottom: 1, + ':first-of-type': { + borderTopLeftRadius: props.theme.radius(2), + borderTopRightRadius: props.theme.radius(2), + }, + ':last-of-type': { + borderBottomLeftRadius: props.theme.radius(2), + borderBottomRightRadius: props.theme.radius(2), + }, +})); + +const AddressContainer = styled.div({ + display: 'flex', + alignItems: 'center', +}); + +const AddressImage = styled.img((props) => ({ + width: 20, + height: 20, + marginRight: props.theme.space.xs, +})); + +const AddressTextTitle = styled.h2((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + textAlign: 'center', +})); + +const TruncatedAddress = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + textAlign: 'right', +})); + +const BnsName = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + textAlign: 'right', +})); + +function AddressPurposeBox({ + purpose, + icon, + title, + address, + bnsName, +}: { + purpose: AddressPurpose; + icon: string; + title: string; + address: string; + bnsName?: string; +}) { + return ( + + + + {title} + +
+ {bnsName ? {bnsName} : null} + {getTruncatedAddress(address)} +
+
+ ); +} + +export default AddressPurposeBox; diff --git a/src/app/screens/authenticationRequest/index.tsx b/src/app/screens/connect/authenticationRequest/index.tsx similarity index 65% rename from src/app/screens/authenticationRequest/index.tsx rename to src/app/screens/connect/authenticationRequest/index.tsx index 0727a19f3..011ecc095 100644 --- a/src/app/screens/authenticationRequest/index.tsx +++ b/src/app/screens/connect/authenticationRequest/index.tsx @@ -1,41 +1,42 @@ +import BitcoinIcon from '@assets/img/dashboard/bitcoin_icon.svg'; +import stxIcon from '@assets/img/dashboard/stx_icon.svg'; import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg'; -import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; import { MESSAGE_SOURCE } from '@common/types/message-types'; import { delay } from '@common/utils/ledger'; -import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import ConfirmScreen from '@components/confirmScreen'; import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import useSeedVault from '@hooks/useSeedVault'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; +import SelectAccount from '@screens/connect/selectAccount'; import { AuthRequest, createAuthResponse, handleLedgerStxJWTAuth, } from '@secretkeylabs/xverse-core'; -import { AddressVersion, publicKeyToAddress, StacksMessageType } from '@stacks/transactions'; +import { AddressVersion, StacksMessageType, publicKeyToAddress } from '@stacks/transactions'; +import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'; import { isHardwareAccount } from '@utils/helper'; import { decodeToken } from 'jsontokens'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { AddressPurpose } from 'sats-connect'; import styled from 'styled-components'; import validUrl from 'valid-url'; +import AddressPurposeBox from '../addressPurposeBox'; +import PermissionsList from '../permissionsList'; -const MainContainer = styled.div({ +const MainContainer = styled.div((props) => ({ display: 'flex', - flex: 1, flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - height: '100%', - overflow: 'hidden', -}); + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + ...props.theme.scrollbar, +})); const SuccessActionsContainer = styled.div((props) => ({ width: '100%', @@ -48,23 +49,30 @@ const SuccessActionsContainer = styled.div((props) => ({ marginTop: props.theme.spacing(20), })); -const TopImage = styled.img({ - aspectRatio: 1, - height: 88, - borderWidth: 10, - borderColor: 'white', -}); +const TopImage = styled.img((props) => ({ + maxHeight: 48, + maxWidth: 48, + marginTop: props.theme.space.xxl, + alignSelf: 'center', +})); -const FunctionTitle = styled.h1((props) => ({ - ...props.theme.headline_s, +const ImagePlaceholder = styled.div((props) => ({ + marginTop: props.theme.space.xxl, +})); + +const Title = styled.h1((props) => ({ + ...props.theme.typography.headline_xs, color: props.theme.colors.white_0, - marginTop: props.theme.spacing(8), + textAlign: 'center', + marginTop: 12, })); -const DappTitle = styled.h2((props) => ({ - ...props.theme.body_l, +const DappName = styled.h2((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_400, marginTop: props.theme.spacing(2), + marginBottom: props.theme.spacing(12), + textAlign: 'center', })); const InfoContainerWrapper = styled.div((props) => ({ @@ -72,6 +80,16 @@ const InfoContainerWrapper = styled.div((props) => ({ marginBottom: 0, })); +const AddressesContainer = styled.div((props) => ({ + marginTop: props.theme.space.s, + width: '100%', +})); + +const PermissionsContainer = styled.div((props) => ({ + width: '100%', + paddingBottom: props.theme.space.xxl, +})); + function AuthenticationRequest() { const [loading, setLoading] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); @@ -82,12 +100,12 @@ function AuthenticationRequest() { const [isTxApproved, setIsTxApproved] = useState(false); const [isTxRejected, setIsTxRejected] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'AUTH_REQUEST_SCREEN' }); - + const navigate = useNavigate(); const { search } = useLocation(); const params = new URLSearchParams(search); const authRequestToken = params.get('authRequest') ?? ''; const authRequest = decodeToken(authRequestToken) as unknown as AuthRequest; - const { selectedAccount } = useWalletSelector(); + const { selectedAccount, btcAddress, stxAddress } = useWalletSelector(); const { getSeed } = useSeedVault(); const isDisabled = !selectedAccount?.stxAddress; @@ -104,6 +122,9 @@ function AuthenticationRequest() { seedPhrase, selectedAccount?.id ?? 0, authRequest, + { + btcAddress: selectedAccount?.btcAddress, + }, ); chrome.tabs.sendMessage(+(params.get('tabId') ?? '0'), { source: MESSAGE_SOURCE, @@ -133,10 +154,15 @@ function AuthenticationRequest() { window.close(); }; - const getDappLogo = () => - validUrl.isWebUri(authRequest?.payload?.appDetails?.icon) - ? authRequest?.payload?.appDetails?.icon - : DappPlaceholderIcon; + const getDappLogo = useCallback( + () => + validUrl.isWebUri(authRequest?.payload?.appDetails?.icon) ? ( + + ) : ( + + ), + [authRequest], + ); const handleConnectAndConfirm = async () => { if (!selectedAccount) { @@ -191,7 +217,6 @@ function AuthenticationRequest() { }); window.close(); } catch (e) { - console.error(e); setIsTxRejected(true); setIsButtonDisabled(false); } finally { @@ -205,36 +230,55 @@ function AuthenticationRequest() { setCurrentStepIndex(0); }; - return ( - - - - - {t('TITLE')} - {`${t('REQUEST_TOOLTIP')} ${authRequest.payload.appDetails?.name}`} - {isDisabled && ( - - { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), - }); + const handleSwitchAccount = () => { + navigate('/account-list?hideListActions=true'); + }; - window.close(); - }} - /> - - )} - + const handleAddStxLedgerAccount = async () => { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`), + }); + + window.close(); + }; + + return ( + + {getDappLogo()} + {t('TITLE')} + {`${t('REQUEST_TOOLTIP')} ${authRequest.payload.appDetails?.name}`} + + + + + + + + + + + + + {isDisabled && ( + + + + )} setIsModalVisible(false)}> {currentStepIndex === 0 && ( - + ); } diff --git a/src/app/screens/connect/btcSelectAddressScreen/helper.ts b/src/app/screens/connect/btcSelectAddressScreen/helper.ts new file mode 100644 index 000000000..1bf47cc69 --- /dev/null +++ b/src/app/screens/connect/btcSelectAddressScreen/helper.ts @@ -0,0 +1,48 @@ +export interface WebManifest { + icons?: { src: string; sizes?: string }[]; +} + +export async function getAppIconFromWebManifest(url: string): Promise { + try { + // Validate URL format + if (!/^https?:\/\/.*/.test(url)) { + throw new Error('Invalid URL format'); + } + + // Fetch the web manifest + const response = await fetch(`${url}/manifest.json`); + + // Check for successful response + if (!response.ok) { + throw new Error(`Failed to fetch web manifest. Status: ${response.status}`); + } + + const manifest: WebManifest = await response.json(); + // Ensure the manifest contains the 'icons' property + if (!manifest.icons || !Array.isArray(manifest.icons)) { + throw new Error('Web manifest is missing the icons property'); + } + + // Extract the app icons' URLs + const icons = manifest.icons.filter((icon) => icon.sizes === '48x48'); + + return `${url}${icons[0].src}`; + } catch (error: any) { + if (error.message.includes('Failed to fetch web manifest')) { + const response = await fetch(`${url}/manifest.webmanifest`); + if (!response.ok) { + return ''; + } + const manifest: WebManifest = await response.json(); + // Ensure the manifest contains the 'icons' property + if (!manifest.icons || !Array.isArray(manifest.icons)) { + throw new Error('Web manifest is missing the icons property'); + } + + // Extract the app icons' URLs + const icons = manifest.icons.filter((icon) => icon.sizes === '48x48'); + return `${url}/${icons[0].src}`; + } + return ''; + } +} diff --git a/src/app/screens/connect/btcSelectAddressScreen/index.tsx b/src/app/screens/connect/btcSelectAddressScreen/index.tsx new file mode 100644 index 000000000..c1573cb83 --- /dev/null +++ b/src/app/screens/connect/btcSelectAddressScreen/index.tsx @@ -0,0 +1,212 @@ +import BitcoinIcon from '@assets/img/dashboard/bitcoin_icon.svg'; +import stxIcon from '@assets/img/dashboard/stx_icon.svg'; +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; +import ActionButton from '@components/button'; +import useBtcAddressRequest from '@hooks/useBtcAddressRequest'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { animated, useSpring } from '@react-spring/web'; +import SelectAccount from '@screens/connect/selectAccount'; +import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { AddressPurpose } from 'sats-connect'; +import styled from 'styled-components'; +import AddressPurposeBox from '../addressPurposeBox'; +import PermissionsList from '../permissionsList'; +import { getAppIconFromWebManifest } from './helper'; + +const OuterContainer = styled(animated.div)((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + ...props.theme.scrollbar, +})); + +const HeadingContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + marginTop: 48, +}); + +const AddressBoxContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + width: '100%', + marginTop: props.theme.spacing(12), +})); + +const TopImage = styled.img((props) => ({ + maxHeight: 48, + borderRadius: props.theme.radius(2), + maxWidth: 48, + marginBottom: props.theme.space.m, +})); + +const Title = styled.h1((props) => ({ + ...props.theme.typography.headline_xs, + color: props.theme.colors.white_0, +})); + +const DapURL = styled.h2((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + marginTop: props.theme.spacing(2), + textAlign: 'center', +})); + +const RequestMessage = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + textAlign: 'left', + wordWrap: 'break-word', + marginTop: props.theme.spacing(12), + marginBottom: props.theme.spacing(12), +})); + +const PermissionsContainer = styled.div((props) => ({ + paddingBottom: props.theme.space.xxl, +})); + +function BtcSelectAddressScreen() { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); + const { network, btcAddress, ordinalsAddress, stxAddress, selectedAccount } = useWalletSelector(); + const [appIcon, setAppIcon] = useState(''); + const { payload, origin, approveBtcAddressRequest, cancelAddressRequest } = + useBtcAddressRequest(); + const appUrl = useMemo(() => origin.replace(/(^\w+:|^)\/\//, ''), [origin]); + + const styles = useSpring({ + from: { + opacity: 0, + y: 24, + }, + to: { + y: 0, + opacity: 1, + }, + }); + + const confirmCallback = async () => { + setLoading(true); + approveBtcAddressRequest(); + window.close(); + }; + + const cancelCallback = () => { + cancelAddressRequest(); + window.close(); + }; + + useEffect(() => { + // Handle address requests to a network that's not currently active + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('NETWORK_MISMATCH_ERROR_TITLE'), + error: t('NETWORK_MISMATCH_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); + } + // Handle address requests with an unsupported purpose + payload.purposes.forEach((purpose) => { + if ( + purpose !== AddressPurpose.Ordinals && + purpose !== AddressPurpose.Payment && + purpose !== AddressPurpose.Stacks + ) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('INVALID_PURPOSE_ERROR_TITLE'), + error: t('INVALID_PURPOSE_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); + } + }); + }, []); + + useEffect(() => { + (async () => { + if (origin !== '') { + getAppIconFromWebManifest(origin).then((appIcons) => { + setAppIcon(appIcons); + }); + } + })(); + + return () => { + setAppIcon(''); + }; + }, [origin]); + + const AddressPurposeRow = useCallback((purpose: AddressPurpose) => { + if (purpose === AddressPurpose.Payment) { + return ( + + ); + } + if (purpose === AddressPurpose.Ordinals) { + return ( + + ); + } + if (purpose === AddressPurpose.Stacks) { + return ( + + ); + } + }, []); + + const handleSwitchAccount = () => { + navigate('/account-list?hideListActions=true'); + }; + + return ( + + + {appIcon !== '' ? : null} + {t('TITLE')} + {appUrl} + + {payload.message ? {payload.message.substring(0, 80)} : null} + + {payload.purposes.map(AddressPurposeRow)} + + + + + + + + + ); +} + +export default BtcSelectAddressScreen; diff --git a/src/app/screens/connect/permissionsList.tsx b/src/app/screens/connect/permissionsList.tsx new file mode 100644 index 000000000..4b23658f6 --- /dev/null +++ b/src/app/screens/connect/permissionsList.tsx @@ -0,0 +1,47 @@ +import { Check } from '@phosphor-icons/react'; +import { useTranslation } from 'react-i18next'; +import styled, { useTheme } from 'styled-components'; + +const PermissionsTitle = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + textAlign: 'left', + marginTop: 24, +})); + +const Permission = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + marginTop: 12, + display: 'flex', + alignItems: 'center', +})); + +const PermissionIcon = styled.div((props) => ({ + display: 'flex', + marginRight: props.theme.space.xs, +})); + +function PermissionsList() { + const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); + const theme = useTheme(); + return ( + <> + {t('PERMISSIONS_TITLE')} + + + + + {t('PERMISSION_WALLET_BALANCE')} + + + + + + {t('PERMISSION_REQUEST_TX')} + + + ); +} + +export default PermissionsList; diff --git a/src/app/screens/connect/selectAccount.tsx b/src/app/screens/connect/selectAccount.tsx new file mode 100644 index 000000000..d74aa6148 --- /dev/null +++ b/src/app/screens/connect/selectAccount.tsx @@ -0,0 +1,100 @@ +import LedgerBadge from '@assets/img/ledger/ledger_badge.svg'; +import { CaretRight } from '@phosphor-icons/react'; +import { Account } from '@secretkeylabs/xverse-core'; +import { getAccountGradient } from '@utils/gradient'; +import { isHardwareAccount } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import styled, { useTheme } from 'styled-components'; + +const AccountInfoContainer = styled.button((props) => ({ + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + borderRadius: 12, + border: `1px solid ${props.theme.colors.white_850}`, + backgroundColor: props.theme.colors.elevation6_800, + padding: props.theme.space.m, +})); + +const CurrentAccountContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', +})); + +const CurrentAccountTextContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: props.theme.spacing(4), + paddingLeft: props.theme.spacing(4), +})); + +const CurrentSelectedAccountText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + textAlign: 'start', +})); + +interface GradientCircleProps { + firstGradient: string; + secondGradient: string; + thirdGradient: string; +} +const GradientCircle = styled.div((props) => ({ + width: 20, + height: 20, + borderRadius: '50%', + background: `linear-gradient(to bottom,${props.firstGradient}, ${props.secondGradient},${props.thirdGradient} )`, +})); + +const SwitchAccountContainer = styled.div(() => ({ + display: 'flex', + alignItems: 'center', +})); +const SwitchAccountText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + marginRight: props.theme.space.xs, + textAlign: 'start', +})); + +type SelectAccountProps = { + account: Account; + handlePressAccount: () => void; +}; + +function SelectAccount({ account, handlePressAccount }: SelectAccountProps) { + const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); + // const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); + const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); + const theme = useTheme(); + const getName = () => + account?.accountName ?? + account?.bnsName ?? + `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`; + + return ( + + + + {account && ( + + {getName()} + {isHardwareAccount(account) && Ledger icon} + + )} + + + {t('CHANGE_ACCOUNT_BUTTON')} + + + + ); +} + +export default SelectAccount; diff --git a/src/assets/img/webInteractions/authPlaceholder.svg b/src/assets/img/webInteractions/authPlaceholder.svg deleted file mode 100644 index 0d5f6780c..000000000 --- a/src/assets/img/webInteractions/authPlaceholder.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/inpage/index.ts b/src/inpage/index.ts index e9b634585..b2cf92e96 100644 --- a/src/inpage/index.ts +++ b/src/inpage/index.ts @@ -5,14 +5,10 @@ import SatsMethodsProvider from './sats.inpage'; import StacksMethodsProvider from './stacks.inpage'; declare global { - interface Window { - XverseProviders: { - StacksProvider: StacksProvider; - BitcoinProvider: BitcoinProvider; - }; + interface XverseProviders { + StacksProvider: StacksProvider; } } - // we inject these in case implementors call the default providers window.StacksProvider = StacksMethodsProvider as StacksProvider; window.BitcoinProvider = SatsMethodsProvider as BitcoinProvider; @@ -20,6 +16,7 @@ window.BitcoinProvider = SatsMethodsProvider as BitcoinProvider; // We also inject the providers in an Xverse object in order to have them exclusively available for Xverse wallet // and not clash with providers from other wallets window.XverseProviders = { + // @ts-ignore StacksProvider: StacksMethodsProvider as StacksProvider, BitcoinProvider: SatsMethodsProvider as BitcoinProvider, }; diff --git a/src/locales/en.json b/src/locales/en.json index 7cb2f7a7f..652787f0e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -494,10 +494,17 @@ "CANCEL_BUTTON": "Cancel" }, "AUTH_REQUEST_SCREEN": { - "TITLE": "Connection", + "TITLE": "Connection Request", + "ACCOUNT": "Account", + "BITCOIN_ADDRESS": "Bitcoin address", + "ORDINAL_ADDRESS": "Ordinal address", + "STX_ADDRESS": "Stacks address", "REQUEST_TOOLTIP": "Requested by", "CONNECT_BUTTON": "Connect", "CANCEL_BUTTON": "Cancel", + "PERMISSIONS_TITLE": "The app will be able to:", + "PERMISSION_WALLET_BALANCE": "See your wallet balance and activity", + "PERMISSION_REQUEST_TX": "Request transaction signing", "NO_STACKS_AUTH_SUPPORT": { "TITLE": "This wallet does not have a Stacks address.", "LINK": "Create an address here" @@ -1036,14 +1043,21 @@ "DO_NOT_SHOW_MESSAGE": "Do not show this message again" }, "SELECT_BTC_ADDRESS_SCREEN": { - "TITLE": "Bitcoin Address Request", - "ACCOUNT": "Account", - "CONNECT_BUTTON": "Approve", - "CANCEL_BUTTON": "Dismiss", - "BITCOIN_ADDRESS": "Bitcoin address", - "ORDINAL_ADDRESS": "Ordinal address", + "TITLE": "Connection Request", + "ACCOUNT_NAME": "Account", + "CONNECT_BUTTON": "Connect", + "CANCEL_BUTTON": "Cancel", + "BITCOIN_ADDRESS": "Payments address", + "ORDINAL_ADDRESS": "Ordinals address", + "STX_ADDRESS": "Stacks address", + "CHANGE_ACCOUNT_BUTTON": "Change account", "NETWORK_MISMATCH_ERROR_TITLE": "Mismatched Network", - "NETWORK_MISMATCH_ERROR_DESCRIPTION": "The app is requesting your wallet address for a different network. You may have to switch your active network in wallet settings." + "NETWORK_MISMATCH_ERROR_DESCRIPTION": "The app is requesting your wallet address for a different network. You may have to switch your active network in wallet settings.", + "INVALID_PURPOSE_ERROR_TITLE": "Invalid Request", + "INVALID_PURPOSE_ERROR_DESCRIPTION": "The app is requesting a wallet address with an invalid purpose. Please contact the developer of the requesting app for support.", + "PERMISSIONS_TITLE": "The app will be able to:", + "PERMISSION_WALLET_BALANCE": "See your wallet balance and activity", + "PERMISSION_REQUEST_TX": "Request transaction signing" }, "SEND_BRC20": { "BRC20_TOKEN": "BRC-20",