diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index e8301d1f7..69a9ef0b4 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -54,7 +54,7 @@ import Initialize, { InitializeContainer } from '@tonkeeper/uikit/dist/pages/imp import { UserThemeProvider } from '@tonkeeper/uikit/dist/providers/UserThemeProvider'; import { useAccountState } from '@tonkeeper/uikit/dist/state/account'; import { useUserFiat } from '@tonkeeper/uikit/dist/state/fiat'; -import { useAuthState } from '@tonkeeper/uikit/dist/state/password'; +import { useAuthState, useCanPromptTouchId } from "@tonkeeper/uikit/dist/state/password"; import { useProBackupState } from '@tonkeeper/uikit/dist/state/pro'; import { useStonfiAssets } from '@tonkeeper/uikit/dist/state/stonfi'; import { useTonendpoint, useTonenpointConfig } from '@tonkeeper/uikit/dist/state/tonendpoint'; @@ -337,6 +337,7 @@ export const Loader: FC = () => { const usePrefetch = () => { useRecommendations(); useStonfiAssets(); + useCanPromptTouchId(); }; export const Content: FC<{ diff --git a/apps/desktop/src/electron/background.ts b/apps/desktop/src/electron/background.ts index b7fe88824..b7960fac2 100644 --- a/apps/desktop/src/electron/background.ts +++ b/apps/desktop/src/electron/background.ts @@ -1,4 +1,4 @@ -import { shell } from 'electron'; +import { shell, systemPreferences, app } from 'electron'; import keytar from 'keytar'; import { Message } from '../libs/message'; import { TonConnectSSE } from './sseEvetns'; @@ -30,6 +30,17 @@ export const handleBackgroundMessage = async (message: Message): Promise { + return sendBackground({ king: 'can-prompt-touch-id' }); + }; + + prompt = async (reason: (lang: string) => string) => { + const lagns = await sendBackground({ + king: 'get-preferred-system-languages' + }); + + const lang = (lagns[0] || 'en').split('-')[0]; + await sendBackground({ + king: 'prompt-touch-id', + reason: reason(lang) + }); + }; +} + export class DesktopAppSdk extends BaseApp implements IAppSdk { keychain = new KeychainDesktop(); @@ -30,5 +48,7 @@ export class DesktopAppSdk extends BaseApp implements IAppSdk { return sendBackground({ king: 'open-page', url }); }; + touchId = new TouchIdDesktop(); + version = packageJson.version ?? 'Unknown'; } diff --git a/apps/desktop/src/libs/message.ts b/apps/desktop/src/libs/message.ts index c4ae0af19..287747f15 100644 --- a/apps/desktop/src/libs/message.ts +++ b/apps/desktop/src/libs/message.ts @@ -43,6 +43,19 @@ export interface TonConnectMessage { king: 'reconnect'; } +export interface GetPreferredSystemLanguagesMessage { + king: 'get-preferred-system-languages'; +} + +export interface CanPromptTouchIdMessage { + king: 'can-prompt-touch-id'; +} + +export interface PromptTouchIdMessage { + king: 'prompt-touch-id'; + reason: string; +} + export type Message = | GetStorageMessage | SetStorageMessage @@ -52,4 +65,7 @@ export type Message = | OpenPageMessage | SetKeychainMessage | GetKeychainMessage - | TonConnectMessage; + | TonConnectMessage + | CanPromptTouchIdMessage + | PromptTouchIdMessage + | GetPreferredSystemLanguagesMessage; diff --git a/packages/core/src/AppSdk.ts b/packages/core/src/AppSdk.ts index 289e589aa..deaa8ce93 100644 --- a/packages/core/src/AppSdk.ts +++ b/packages/core/src/AppSdk.ts @@ -62,6 +62,11 @@ export interface KeychainPassword { getPassword: (publicKey: string) => Promise; } +export interface TouchId { + canPrompt: () => Promise; + prompt: (reason: (lang: string) => string) => Promise; +} + export interface NotificationService { subscribe: (wallet: WalletState, mnemonic: string[]) => Promise; unsubscribe: (address?: string) => Promise; @@ -76,6 +81,7 @@ export interface IAppSdk { storage: IStorage; nativeBackButton?: NativeBackButton; keychain?: KeychainPassword; + touchId?: TouchId; topMessage: (text: string) => void; copyToClipboard: (value: string, notification?: string) => void; diff --git a/packages/core/src/Keys.ts b/packages/core/src/Keys.ts index 30e51d6b9..8a8aae0ab 100644 --- a/packages/core/src/Keys.ts +++ b/packages/core/src/Keys.ts @@ -10,6 +10,7 @@ export enum AppKey { GLOBAL_AUTH_STATE = 'password', LOCK = 'lock', + TOUCH_ID = 'touch_id', COUNTRY = 'country', FAVOURITES = 'favourites', diff --git a/packages/locales/src/tonkeeper-web/en.json b/packages/locales/src/tonkeeper-web/en.json index b467b01f8..e20f1ca67 100644 --- a/packages/locales/src/tonkeeper-web/en.json +++ b/packages/locales/src/tonkeeper-web/en.json @@ -127,6 +127,7 @@ "ton_login_title_web" : "Connect to {name}?", "Ton_page_description" : "TON is a fully decentralized layer-1 blockchain designed by Telegram to onboard billions of users. It boasts ultra-fast transactions, tiny fees, easy-to-use apps, and is environmentally friendly.", "total_balance" : "Total balance", + "touch_id_unlock_wallet" : "unlock your wallet", "transaction_call_date" : "Contract Call %{date}", "transaction_type_mint" : "Mint", "transaction_type_purchase" : "Purchase", diff --git a/packages/locales/src/tonkeeper-web/ru-RU.json b/packages/locales/src/tonkeeper-web/ru-RU.json index 3c245aa6d..b056f6f37 100644 --- a/packages/locales/src/tonkeeper-web/ru-RU.json +++ b/packages/locales/src/tonkeeper-web/ru-RU.json @@ -126,6 +126,7 @@ "ton_login_title_web" : "Войти в {name}?", "Ton_page_description" : "TON — это полностью децентрализованный блокчейн первого уровня, разработанный Telegram для миллиардов пользователей. Он может похвастаться сверхбыстрыми транзакциями, небольшими комиссиями, простыми в использовании приложениями и экологичностью.", "total_balance" : "Баланс", + "touch_id_unlock_wallet" : "разблокировать ваш кошелек", "transaction_call_date" : "Вызов контракта %{date}", "transaction_type_mint" : "Создание", "transaction_type_purchase" : "Покупка", diff --git a/packages/uikit/src/components/Icon.tsx b/packages/uikit/src/components/Icon.tsx index e50c68f41..813ed9cca 100644 --- a/packages/uikit/src/components/Icon.tsx +++ b/packages/uikit/src/components/Icon.tsx @@ -1557,3 +1557,22 @@ export const ResponsiveSpinner: FC<{ className?: string }> = ({ className }) => return ; }; + +export const LockIcon = () => { + return ( + + + + ); +}; diff --git a/packages/uikit/src/components/connect/TonConnectNotification.tsx b/packages/uikit/src/components/connect/TonConnectNotification.tsx index 00de8d313..c74bc0a79 100644 --- a/packages/uikit/src/components/connect/TonConnectNotification.tsx +++ b/packages/uikit/src/components/connect/TonConnectNotification.tsx @@ -28,6 +28,7 @@ import { Notification, NotificationBlock } from '../Notification'; import { Body2, Body3, H2, Label2 } from '../Text'; import { Button } from '../fields/Button'; import { ResultButton } from '../transfer/common'; +import { useCheckTouchId } from '../../state/password'; const useConnectMutation = ( request: ConnectRequest, @@ -38,6 +39,7 @@ const useConnectMutation = ( const sdk = useAppSdk(); const client = useQueryClient(); const { t } = useTranslation(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async () => { const params = await getTonConnectParams(request); @@ -49,7 +51,7 @@ const useConnectMutation = ( result.push(toTonAddressItemReply(wallet)); } if (item.name === 'ton_proof') { - const signTonConnect = signTonConnectOver(sdk, wallet.publicKey, t); + const signTonConnect = signTonConnectOver(sdk, wallet.publicKey, t, checkTouchId); const proof = tonConnectProofPayload( webViewUrl ?? manifest.url, wallet.active.rawAddress, diff --git a/packages/uikit/src/components/connect/TonTransactionNotification.tsx b/packages/uikit/src/components/connect/TonTransactionNotification.tsx index fc21dafc2..e30a7a425 100644 --- a/packages/uikit/src/components/connect/TonTransactionNotification.tsx +++ b/packages/uikit/src/components/connect/TonTransactionNotification.tsx @@ -28,6 +28,7 @@ import { Button } from '../fields/Button'; import { ResultButton } from '../transfer/common'; import { EmulationList } from './EstimationLayout'; import { TxConfirmationCustomError } from '../../libs/errors/TxConfirmationCustomError'; +import { useCheckTouchId } from '../../state/password'; const ButtonGap = styled.div` ${props => @@ -56,13 +57,14 @@ const useSendMutation = (params: TonConnectTransactionPayload, estimate?: Estima const { api } = useAppContext(); const client = useQueryClient(); const { t } = useTranslation(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async () => { const accounts = estimate?.accounts; if (!accounts) { throw new Error('Missing accounts data'); } - const signer = await getSigner(sdk, wallet.publicKey); + const signer = await getSigner(sdk, wallet.publicKey, checkTouchId); if (signer.type !== 'cell') { throw new TxConfirmationCustomError(t('ledger_operation_not_supported')); } @@ -161,10 +163,14 @@ const ConnectContent: FC<{ }, []); const onSubmit = async () => { - const result = await mutateAsync(); - setDone(true); - sdk.hapticNotification('success'); - setTimeout(() => handleClose(result), 300); + try { + const result = await mutateAsync(); + setDone(true); + sdk.hapticNotification('success'); + setTimeout(() => handleClose(result), 300); + } catch (e) { + console.error(e); + } }; if (issues?.kind !== undefined) { diff --git a/packages/uikit/src/components/desktop/aside/PreferencesAsideMenu.tsx b/packages/uikit/src/components/desktop/aside/PreferencesAsideMenu.tsx index 03aeb4b26..c47773745 100644 --- a/packages/uikit/src/components/desktop/aside/PreferencesAsideMenu.tsx +++ b/packages/uikit/src/components/desktop/aside/PreferencesAsideMenu.tsx @@ -9,6 +9,7 @@ import { EnvelopeIcon, ExitIcon, GlobeIcon, + LockIcon, PlaceIcon, SlidersIcon, TelegramIcon, @@ -101,6 +102,14 @@ export const PreferencesAsideMenu = () => { )} + + {({ isActive }) => ( + + + {t('settings_security')} + + )} + {({ isActive }) => ( diff --git a/packages/uikit/src/components/transfer/nft/ConfirmNftView.tsx b/packages/uikit/src/components/transfer/nft/ConfirmNftView.tsx index bee04b2ac..ab94379c2 100644 --- a/packages/uikit/src/components/transfer/nft/ConfirmNftView.tsx +++ b/packages/uikit/src/components/transfer/nft/ConfirmNftView.tsx @@ -33,6 +33,7 @@ import { } from '../ConfirmView'; import { NftDetailsBlock } from './Common'; import { TxConfirmationCustomError } from '../../../libs/errors/TxConfirmationCustomError'; +import { useCheckTouchId } from '../../../state/password'; const assetAmount = new AssetAmount({ asset: TON_ASSET, @@ -76,11 +77,12 @@ const useSendNft = ( const wallet = useWalletContext(); const client = useQueryClient(); const track2 = useTransactionAnalytics(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async () => { if (!fee) return false; - const signer = await getSigner(sdk, wallet.publicKey).catch(() => null); + const signer = await getSigner(sdk, wallet.publicKey, checkTouchId).catch(() => null); if (signer?.type !== 'cell') { throw new TxConfirmationCustomError(t('ledger_operation_not_supported')); } diff --git a/packages/uikit/src/desktop-pages/preferences/DesktopPreferencesRouting.tsx b/packages/uikit/src/desktop-pages/preferences/DesktopPreferencesRouting.tsx index 1d8fcb9ff..9956eab4f 100644 --- a/packages/uikit/src/desktop-pages/preferences/DesktopPreferencesRouting.tsx +++ b/packages/uikit/src/desktop-pages/preferences/DesktopPreferencesRouting.tsx @@ -10,6 +10,7 @@ import { Account } from '../../pages/settings/Account'; import { Notifications } from '../../pages/settings/Notification'; import { CountrySettings } from '../../pages/settings/Country'; import styled from 'styled-components'; +import { SecuritySettings } from '../../pages/settings/Security'; const OldSettingsLayoutWrapper = styled.div` padding-top: 64px; @@ -52,12 +53,7 @@ export const DesktopPreferencesRouting = () => { } /> - - } - /> + } /> } /> } /> } /> diff --git a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx index 1eb44b500..41840e961 100644 --- a/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx +++ b/packages/uikit/src/desktop-pages/settings/DesktopWalletSettingsRouting.tsx @@ -30,7 +30,6 @@ export const DesktopWalletSettingsRouting = () => { } /> } /> - } /> } /> diff --git a/packages/uikit/src/hooks/blockchain/useExecuteTonContract.ts b/packages/uikit/src/hooks/blockchain/useExecuteTonContract.ts index 15198cc92..bda62166b 100644 --- a/packages/uikit/src/hooks/blockchain/useExecuteTonContract.ts +++ b/packages/uikit/src/hooks/blockchain/useExecuteTonContract.ts @@ -11,6 +11,7 @@ import { useAppContext, useWalletContext } from '../appContext'; import { useAppSdk } from '../appSdk'; import { useTranslation } from '../translation'; import { TxConfirmationCustomError } from '../../libs/errors/TxConfirmationCustomError'; +import { useCheckTouchId } from '../../state/password'; export type ContractExecutorParams = { api: APIConfig; @@ -35,13 +36,14 @@ export function useExecuteTonContract( const walletState = useWalletContext(); const client = useQueryClient(); const track2 = useTransactionAnalytics(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async () => { if (!args.fee) { return false; } - const signer = await getSigner(sdk, walletState.publicKey).catch(() => null); + const signer = await getSigner(sdk, walletState.publicKey, checkTouchId).catch(() => null); if (signer?.type !== 'cell') { throw new TxConfirmationCustomError(t('ledger_operation_not_supported')); } diff --git a/packages/uikit/src/hooks/blockchain/useSendMultiTransfer.ts b/packages/uikit/src/hooks/blockchain/useSendMultiTransfer.ts index 6def1af88..4ff8be39a 100644 --- a/packages/uikit/src/hooks/blockchain/useSendMultiTransfer.ts +++ b/packages/uikit/src/hooks/blockchain/useSendMultiTransfer.ts @@ -17,6 +17,7 @@ import { useAppContext, useWalletContext } from '../appContext'; import { useAppSdk } from '../appSdk'; import { useTranslation } from '../translation'; import { TxConfirmationCustomError } from '../../libs/errors/TxConfirmationCustomError'; +import { useCheckTouchId } from '../../state/password'; export type MultiSendFormTokenized = { rows: { @@ -45,13 +46,14 @@ export function useSendMultiTransfer() { const client = useQueryClient(); const track2 = useTransactionAnalytics(); const { data: jettons } = useWalletJettonList(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation< boolean, Error, { form: MultiSendFormTokenized; asset: TonAsset; feeEstimation: BigNumber } >(async ({ form, asset, feeEstimation }) => { - const signer = await getSigner(sdk, wallet.publicKey).catch(() => null); + const signer = await getSigner(sdk, wallet.publicKey, checkTouchId).catch(() => null); if (signer === null) return false; try { if (signer.type !== 'cell') { diff --git a/packages/uikit/src/hooks/blockchain/useSendTransfer.ts b/packages/uikit/src/hooks/blockchain/useSendTransfer.ts index 7630f340d..6a3bd1e11 100644 --- a/packages/uikit/src/hooks/blockchain/useSendTransfer.ts +++ b/packages/uikit/src/hooks/blockchain/useSendTransfer.ts @@ -19,6 +19,7 @@ import { useTransactionAnalytics } from '../amplitude'; import { useAppContext, useWalletContext } from '../appContext'; import { useAppSdk } from '../appSdk'; import { useTranslation } from '../translation'; +import { useCheckTouchId } from '../../state/password'; export function useSendTransfer( recipient: T extends TonAsset ? TonRecipientData : TronRecipientData, @@ -33,9 +34,10 @@ export function useSendTransfer( const client = useQueryClient(); const track2 = useTransactionAnalytics(); const { data: jettons } = useWalletJettonList(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async () => { - const signer = await getSigner(sdk, wallet.publicKey).catch(() => null); + const signer = await getSigner(sdk, wallet.publicKey, checkTouchId).catch(() => null); if (signer === null) return false; try { if (isTonAsset(amount.asset)) { diff --git a/packages/uikit/src/libs/queryKey.ts b/packages/uikit/src/libs/queryKey.ts index 0ab0dac5d..6998bdce1 100644 --- a/packages/uikit/src/libs/queryKey.ts +++ b/packages/uikit/src/libs/queryKey.ts @@ -3,6 +3,8 @@ export enum QueryKey { wallet = 'wallet', wallets = 'wallets', lock = 'lock', + touchId = 'touchId', + canPromptTouchId = 'canPromptTouchId', country = 'country', password = 'password', addresses = 'addresses', diff --git a/packages/uikit/src/libs/routes.ts b/packages/uikit/src/libs/routes.ts index 332debedb..62bb39cc2 100644 --- a/packages/uikit/src/libs/routes.ts +++ b/packages/uikit/src/libs/routes.ts @@ -50,8 +50,7 @@ export enum WalletSettingsRoute { index = '/', recovery = '/recovery', version = '/version', - jettons = '/jettons', - security = '/security' + jettons = '/jettons' } export enum BrowserRoute { diff --git a/packages/uikit/src/pages/settings/Notification.tsx b/packages/uikit/src/pages/settings/Notification.tsx index 47bef1682..2ea183dc4 100644 --- a/packages/uikit/src/pages/settings/Notification.tsx +++ b/packages/uikit/src/pages/settings/Notification.tsx @@ -11,6 +11,7 @@ import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { QueryKey } from '../../libs/queryKey'; import { getMnemonic } from '../../state/mnemonic'; +import { useCheckTouchId } from '../../state/password'; const useSubscribed = () => { const sdk = useAppSdk(); @@ -32,6 +33,7 @@ const useToggleSubscribe = () => { const sdk = useAppSdk(); const wallet = useWalletContext(); const client = useQueryClient(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); return useMutation(async checked => { const { notifications } = sdk; @@ -39,7 +41,7 @@ const useToggleSubscribe = () => { throw new Error('Missing notifications'); } if (checked) { - const mnemonic = await getMnemonic(sdk, wallet.publicKey); + const mnemonic = await getMnemonic(sdk, wallet.publicKey, checkTouchId); try { await notifications.subscribe(wallet, mnemonic); } catch (e) { diff --git a/packages/uikit/src/pages/settings/Recovery.tsx b/packages/uikit/src/pages/settings/Recovery.tsx index 75b5f237e..cac02e90c 100644 --- a/packages/uikit/src/pages/settings/Recovery.tsx +++ b/packages/uikit/src/pages/settings/Recovery.tsx @@ -9,6 +9,7 @@ import { useAppContext, useWalletContext } from '../../hooks/appContext'; import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { getMnemonic } from '../../state/mnemonic'; +import { useCheckTouchId } from "../../state/password"; export const ActiveRecovery = () => { const wallet = useWalletContext(); @@ -28,11 +29,12 @@ const useMnemonic = (publicKey: string, auth: AuthState) => { const [mnemonic, setMnemonic] = useState(undefined); const sdk = useAppSdk(); const navigate = useNavigate(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); useEffect(() => { (async () => { try { - setMnemonic(await getMnemonic(sdk, publicKey)); + setMnemonic(await getMnemonic(sdk, publicKey, checkTouchId)); } catch (e) { navigate(-1); } diff --git a/packages/uikit/src/pages/settings/Security.tsx b/packages/uikit/src/pages/settings/Security.tsx index 2f90ba1fa..5037fe8fa 100644 --- a/packages/uikit/src/pages/settings/Security.tsx +++ b/packages/uikit/src/pages/settings/Security.tsx @@ -11,7 +11,13 @@ import { SettingsItem, SettingsList } from '../../components/settings/SettingsLi import { useAppContext } from '../../hooks/appContext'; import { useTranslation } from '../../hooks/translation'; import { AppRoute, SettingsRoute } from '../../libs/routes'; -import { useLookScreen, useMutateLookScreen } from '../../state/password'; +import { + useCanPromptTouchId, + useLookScreen, + useMutateLookScreen, + useMutateTouchId, + useTouchIdEnabled +} from '../../state/password'; const LockSwitch = () => { const { t } = useTranslation(); @@ -37,6 +43,31 @@ const LockSwitch = () => { } }; +const TouchIdSwitch = () => { + const { t } = useTranslation(); + const { data: canPrompt } = useCanPromptTouchId(); + + const { data: touchIdEnabled } = useTouchIdEnabled(); + const { mutate } = useMutateTouchId(); + + console.log(touchIdEnabled); + + if (!canPrompt) { + return null; + } + + return ( + + + + {t('biometry_ios_fingerprint')} + + + + + ); +}; + const ChangePassword = () => { const { t } = useTranslation(); const [isOpen, setOpen] = useState(false); @@ -90,6 +121,7 @@ export const SecuritySettings = () => { + diff --git a/packages/uikit/src/state/mnemonic.ts b/packages/uikit/src/state/mnemonic.ts index 01992f1ce..218e34102 100644 --- a/packages/uikit/src/state/mnemonic.ts +++ b/packages/uikit/src/state/mnemonic.ts @@ -17,7 +17,8 @@ import { TxConfirmationCustomError } from '../libs/errors/TxConfirmationCustomEr export const signTonConnectOver = ( sdk: IAppSdk, publicKey: string, - t: (text: string) => string + t: (text: string) => string, + checkTouchId: () => Promise ) => { return async (bufferToSign: Buffer) => { const auth = await getWalletAuthState(sdk.storage, publicKey); @@ -36,7 +37,7 @@ export const signTonConnectOver = ( throw new TxConfirmationCustomError(t('ledger_operation_not_supported')); } default: { - const mnemonic = await getMnemonic(sdk, publicKey); + const mnemonic = await getMnemonic(sdk, publicKey, checkTouchId); const keyPair = await mnemonicToPrivateKey(mnemonic); const signature = nacl.sign.detached( Buffer.from(sha256_sync(bufferToSign)), @@ -48,58 +49,71 @@ export const signTonConnectOver = ( }; }; -export const getSigner = async (sdk: IAppSdk, publicKey: string): Promise => { - const auth = await getWalletAuthState(sdk.storage, publicKey); - - switch (auth.kind) { - case 'signer': { - const callback = async (message: Cell) => { - const result = await pairSignerByNotification( - sdk, - message.toBoc({ idx: false }).toString('base64') - ); - return parseSignerSignature(result); - }; - callback.type = 'cell' as const; - return callback; - } - case 'ledger': { - const callback = async (path: number[], transaction: LedgerTransaction) => - pairLedgerByNotification(sdk, path, transaction); - callback.type = 'ledger' as const; - return callback; - } - case 'signer-deeplink': { - const callback = async (message: Cell) => { - const deeplink = await storeTransactionAndCreateDeepLink( - sdk, - publicKey, - message.toBoc({ idx: false }).toString('base64') - ); - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - window.location = deeplink as any; - - await delay(2000); +export const getSigner = async ( + sdk: IAppSdk, + publicKey: string, + checkTouchId: () => Promise +): Promise => { + try { + const auth = await getWalletAuthState(sdk.storage, publicKey); - throw new Error('Navigate to deeplink'); - }; - callback.type = 'cell' as const; - return callback as CellSigner; - } - default: { - const mnemonic = await getMnemonic(sdk, publicKey); - const callback = async (message: Cell) => { - const keyPair = await mnemonicToPrivateKey(mnemonic); - return sign(message.hash(), keyPair.secretKey); - }; - callback.type = 'cell' as const; - return callback; + switch (auth.kind) { + case 'signer': { + const callback = async (message: Cell) => { + const result = await pairSignerByNotification( + sdk, + message.toBoc({ idx: false }).toString('base64') + ); + return parseSignerSignature(result); + }; + callback.type = 'cell' as const; + return callback; + } + case 'ledger': { + const callback = async (path: number[], transaction: LedgerTransaction) => + pairLedgerByNotification(sdk, path, transaction); + callback.type = 'ledger' as const; + return callback; + } + case 'signer-deeplink': { + const callback = async (message: Cell) => { + const deeplink = await storeTransactionAndCreateDeepLink( + sdk, + publicKey, + message.toBoc({ idx: false }).toString('base64') + ); + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + window.location = deeplink as any; + + await delay(2000); + + throw new Error('Navigate to deeplink'); + }; + callback.type = 'cell' as const; + return callback as CellSigner; + } + default: { + const mnemonic = await getMnemonic(sdk, publicKey, checkTouchId); + const callback = async (message: Cell) => { + const keyPair = await mnemonicToPrivateKey(mnemonic); + return sign(message.hash(), keyPair.secretKey); + }; + callback.type = 'cell' as const; + return callback; + } } + } catch (e) { + console.error(e); + throw e; } }; -export const getMnemonic = async (sdk: IAppSdk, publicKey: string): Promise => { +export const getMnemonic = async ( + sdk: IAppSdk, + publicKey: string, + checkTouchId: () => Promise +): Promise => { const auth = await getWalletAuthState(sdk.storage, publicKey); switch (auth.kind) { @@ -114,6 +128,7 @@ export const getMnemonic = async (sdk: IAppSdk, publicKey: string): Promise { const sdk = useAppSdk(); @@ -28,3 +30,53 @@ export const useMutateLookScreen = () => { await client.invalidateQueries([QueryKey.lock]); }); }; + +export const useCanPromptTouchId = () => { + const sdk = useAppSdk(); + return useQuery([QueryKey.canPromptTouchId], async () => { + return sdk.touchId?.canPrompt(); + }); +}; + +export const useTouchIdEnabled = () => { + const sdk = useAppSdk(); + return useQuery([QueryKey.touchId], async () => { + return isTouchIdEnabled(sdk); + }); +}; + +const isTouchIdEnabled = async (sdk: IAppSdk): Promise => { + const canPrompt = await sdk.touchId?.canPrompt(); + if (!canPrompt) { + return false; + } + + const touchId = await sdk.storage.get(AppKey.TOUCH_ID); + + return touchId ?? true; +}; + +export const useMutateTouchId = () => { + const sdk = useAppSdk(); + const client = useQueryClient(); + return useMutation(async value => { + await sdk.storage.set(AppKey.TOUCH_ID, value); + await client.invalidateQueries([QueryKey.touchId]); + }); +}; + +export const useCheckTouchId = () => { + const sdk = useAppSdk(); + const { t } = useTranslation(); + return useMutation(async () => { + const touchId = await isTouchIdEnabled(sdk); + if (touchId) { + await sdk.touchId?.prompt(lng => + (t as (val: string, options?: { lng?: string }) => string)( + 'touch_id_unlock_wallet', + { lng } + ) + ); + } + }); +}; diff --git a/packages/uikit/src/state/pro.ts b/packages/uikit/src/state/pro.ts index 5a882bdc7..7ac370e35 100644 --- a/packages/uikit/src/state/pro.ts +++ b/packages/uikit/src/state/pro.ts @@ -24,6 +24,7 @@ import { useAppSdk } from '../hooks/appSdk'; import { useTranslation } from '../hooks/translation'; import { QueryKey } from '../libs/queryKey'; import { signTonConnectOver } from './mnemonic'; +import { useCheckTouchId } from './password'; export const useProBackupState = () => { const sdk = useAppSdk(); @@ -50,12 +51,14 @@ export const useSelectWalletMutation = () => { const sdk = useAppSdk(); const client = useQueryClient(); const { t } = useTranslation(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); + return useMutation(async publicKey => { const state = await getWalletState(sdk.storage, publicKey); if (!state) { throw new Error('Missing wallet state'); } - await authViaTonConnect(state, signTonConnectOver(sdk, publicKey, t)); + await authViaTonConnect(state, signTonConnectOver(sdk, publicKey, t, checkTouchId)); await client.invalidateQueries([QueryKey.pro]); }); diff --git a/packages/uikit/src/state/tron/tron.ts b/packages/uikit/src/state/tron/tron.ts index cf355a51a..810fd3556 100644 --- a/packages/uikit/src/state/tron/tron.ts +++ b/packages/uikit/src/state/tron/tron.ts @@ -11,6 +11,7 @@ import { useAppSdk } from '../../hooks/appSdk'; import { QueryKey } from '../../libs/queryKey'; import { getMnemonic } from '../mnemonic'; import { DefaultRefetchInterval } from '../tonendpoint'; +import { useCheckTouchId } from '../password'; enum TronKeys { state, @@ -24,6 +25,8 @@ export const useTronWalletState = (enabled = true) => { } = useAppContext(); const wallet = useWalletContext(); const client = useQueryClient(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); + return useQuery( [wallet.publicKey, QueryKey.tron, wallet.network, TronKeys.state], async () => { @@ -31,7 +34,7 @@ export const useTronWalletState = (enabled = true) => { return getTronWalletState(wallet.tron, wallet.network); } - const mnemonic = await getMnemonic(sdk, wallet.publicKey); + const mnemonic = await getMnemonic(sdk, wallet.publicKey, checkTouchId); const tron = await importTronWallet(sdk.storage, tronApi, wallet, mnemonic); const result = getTronWalletState(tron, wallet.network);