Skip to content

Commit

Permalink
Merge pull request #82 from tonkeeper/feature/touch-id
Browse files Browse the repository at this point in the history
Touch Id verification for Desktop macOS
  • Loading branch information
KuznetsovNikita authored May 8, 2024
2 parents db09107 + a430131 commit 7e48876
Show file tree
Hide file tree
Showing 27 changed files with 283 additions and 77 deletions.
3 changes: 2 additions & 1 deletion apps/desktop/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -337,6 +337,7 @@ export const Loader: FC = () => {
const usePrefetch = () => {
useRecommendations();
useStonfiAssets();
useCanPromptTouchId();
};

export const Content: FC<{
Expand Down
13 changes: 12 additions & 1 deletion apps/desktop/src/electron/background.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,6 +30,17 @@ export const handleBackgroundMessage = async (message: Message): Promise<unknown
return await keytar.getPassword(service, `Wallet-${message.publicKey}`);
case 'reconnect':
return await TonConnectSSE.getInstance().reconnect();
case 'can-prompt-touch-id':
try {
return !!systemPreferences?.canPromptTouchID?.();
} catch (e) {
console.error(e);
return false;
}
case 'prompt-touch-id':
return systemPreferences.promptTouchID(message.reason);
case 'get-preferred-system-languages':
return app.getPreferredSystemLanguages();
default:
throw new Error(`Unknown message: ${JSON.stringify(message)}`);
}
Expand Down
22 changes: 21 additions & 1 deletion apps/desktop/src/libs/appSdk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseApp, IAppSdk, KeychainPassword } from '@tonkeeper/core/dist/AppSdk';
import { BaseApp, IAppSdk, KeychainPassword, TouchId } from '@tonkeeper/core/dist/AppSdk';
import copyToClipboard from 'copy-to-clipboard';
import packageJson from '../../package.json';
import { sendBackground } from './backgroudService';
Expand All @@ -13,6 +13,24 @@ export class KeychainDesktop implements KeychainPassword {
};
}

export class TouchIdDesktop implements TouchId {
canPrompt = async () => {
return sendBackground<boolean>({ king: 'can-prompt-touch-id' });
};

prompt = async (reason: (lang: string) => string) => {
const lagns = await sendBackground<string[]>({
king: 'get-preferred-system-languages'
});

const lang = (lagns[0] || 'en').split('-')[0];
await sendBackground<void>({
king: 'prompt-touch-id',
reason: reason(lang)
});
};
}

export class DesktopAppSdk extends BaseApp implements IAppSdk {
keychain = new KeychainDesktop();

Expand All @@ -30,5 +48,7 @@ export class DesktopAppSdk extends BaseApp implements IAppSdk {
return sendBackground<void>({ king: 'open-page', url });
};

touchId = new TouchIdDesktop();

version = packageJson.version ?? 'Unknown';
}
18 changes: 17 additions & 1 deletion apps/desktop/src/libs/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,4 +65,7 @@ export type Message =
| OpenPageMessage
| SetKeychainMessage
| GetKeychainMessage
| TonConnectMessage;
| TonConnectMessage
| CanPromptTouchIdMessage
| PromptTouchIdMessage
| GetPreferredSystemLanguagesMessage;
6 changes: 6 additions & 0 deletions packages/core/src/AppSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface KeychainPassword {
getPassword: (publicKey: string) => Promise<string>;
}

export interface TouchId {
canPrompt: () => Promise<boolean>;
prompt: (reason: (lang: string) => string) => Promise<void>;
}

export interface NotificationService {
subscribe: (wallet: WalletState, mnemonic: string[]) => Promise<void>;
unsubscribe: (address?: string) => Promise<void>;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/Keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum AppKey {

GLOBAL_AUTH_STATE = 'password',
LOCK = 'lock',
TOUCH_ID = 'touch_id',
COUNTRY = 'country',

FAVOURITES = 'favourites',
Expand Down
1 change: 1 addition & 0 deletions packages/locales/src/tonkeeper-web/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/locales/src/tonkeeper-web/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" : "Покупка",
Expand Down
19 changes: 19 additions & 0 deletions packages/uikit/src/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1557,3 +1557,22 @@ export const ResponsiveSpinner: FC<{ className?: string }> = ({ className }) =>

return <SpinnerIcon className={className} />;
};

export const LockIcon = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 4V6H6V4C6 2.89543 6.89543 2 8 2C9.10457 2 10 2.89543 10 4ZM4.5 6.0143V6V4C4.5 2.067 6.067 0.5 8 0.5C9.933 0.5 11.5 2.067 11.5 4V6V6.0143C12.1748 6.03943 12.6236 6.10871 12.9985 6.29973C13.5159 6.56338 13.9366 6.98408 14.2003 7.50153C14.5 8.08978 14.5 8.85986 14.5 10.4V11.1C14.5 12.6401 14.5 13.4102 14.2003 13.9985C13.9366 14.5159 13.5159 14.9366 12.9985 15.2003C12.4102 15.5 11.6401 15.5 10.1 15.5H5.9C4.35986 15.5 3.58978 15.5 3.00153 15.2003C2.48408 14.9366 2.06338 14.5159 1.79973 13.9985C1.5 13.4102 1.5 12.6401 1.5 11.1V10.4C1.5 8.85986 1.5 8.08978 1.79973 7.50153C2.06338 6.98408 2.48408 6.56338 3.00153 6.29973C3.37642 6.10871 3.82516 6.03943 4.5 6.0143ZM7.25 9.25C7.25 8.83579 7.58579 8.5 8 8.5C8.41421 8.5 8.75 8.83579 8.75 9.25V10.75C8.75 11.1642 8.41421 11.5 8 11.5C7.58579 11.5 7.25 11.1642 7.25 10.75V9.25Z"
fill="currentColor"
/>
</svg>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,6 +39,7 @@ const useConnectMutation = (
const sdk = useAppSdk();
const client = useQueryClient();
const { t } = useTranslation();
const { mutateAsync: checkTouchId } = useCheckTouchId();

return useMutation<ConnectItemReply[], Error>(async () => {
const params = await getTonConnectParams(request);
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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<string, Error>(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'));
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EnvelopeIcon,
ExitIcon,
GlobeIcon,
LockIcon,
PlaceIcon,
SlidersIcon,
TelegramIcon,
Expand Down Expand Up @@ -101,6 +102,14 @@ export const PreferencesAsideMenu = () => {
</AsideMenuItemStyled>
)}
</NavLink>
<NavLink to={AppRoute.settings + SettingsRoute.security}>
{({ isActive }) => (
<AsideMenuItemStyled isSelected={isActive || isCoinPageOpened}>
<LockIcon />
<Label2>{t('settings_security')}</Label2>
</AsideMenuItemStyled>
)}
</NavLink>
<NavLink to={AppRoute.settings + SettingsRoute.pro}>
{({ isActive }) => (
<AsideMenuItemStyled isSelected={isActive}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,11 +77,12 @@ const useSendNft = (
const wallet = useWalletContext();
const client = useQueryClient();
const track2 = useTransactionAnalytics();
const { mutateAsync: checkTouchId } = useCheckTouchId();

return useMutation<boolean, Error>(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'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,12 +53,7 @@ export const DesktopPreferencesRouting = () => {
<Navigate to={AppRoute.walletSettings + WalletSettingsRoute.jettons} />
}
/>
<Route
path={SettingsRoute.security}
element={
<Navigate to={AppRoute.walletSettings + WalletSettingsRoute.security} />
}
/>
<Route path={SettingsRoute.security} element={<SecuritySettings />} />
<Route path={SettingsRoute.country} element={<CountrySettings />} />
<Route path={SettingsRoute.pro} element={<ProSettings />} />
<Route path="*" element={<Navigate to={'.' + SettingsRoute.account} replace />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const DesktopWalletSettingsRouting = () => {
</Route>
<Route path={SettingsRoute.version} element={<WalletVersion />} />
<Route path={SettingsRoute.jettons} element={<JettonsSettings />} />
<Route path={SettingsRoute.security} element={<SecuritySettings />} />
</Route>
<Route path="*" element={<DesktopWalletSettingsPage />} />
</Routes>
Expand Down
4 changes: 3 additions & 1 deletion packages/uikit/src/hooks/blockchain/useExecuteTonContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,13 +36,14 @@ export function useExecuteTonContract<Args extends ContractExecutorParams>(
const walletState = useWalletContext();
const client = useQueryClient();
const track2 = useTransactionAnalytics();
const { mutateAsync: checkTouchId } = useCheckTouchId();

return useMutation<boolean, Error>(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'));
}
Expand Down
4 changes: 3 additions & 1 deletion packages/uikit/src/hooks/blockchain/useSendMultiTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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') {
Expand Down
4 changes: 3 additions & 1 deletion packages/uikit/src/hooks/blockchain/useSendTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Asset>(
recipient: T extends TonAsset ? TonRecipientData : TronRecipientData,
Expand All @@ -33,9 +34,10 @@ export function useSendTransfer<T extends Asset>(
const client = useQueryClient();
const track2 = useTransactionAnalytics();
const { data: jettons } = useWalletJettonList();
const { mutateAsync: checkTouchId } = useCheckTouchId();

return useMutation<boolean, Error>(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)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/uikit/src/libs/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export enum QueryKey {
wallet = 'wallet',
wallets = 'wallets',
lock = 'lock',
touchId = 'touchId',
canPromptTouchId = 'canPromptTouchId',
country = 'country',
password = 'password',
addresses = 'addresses',
Expand Down
Loading

0 comments on commit 7e48876

Please sign in to comment.