Support Passkeys (#6535)
This commit is contained in:
parent
01d7cf294d
commit
41c2a17fdc
@ -120,6 +120,8 @@ export interface GramJsAppConfig extends LimitsConfig {
|
|||||||
verify_age_min?: number;
|
verify_age_min?: number;
|
||||||
message_typing_draft_ttl?: number;
|
message_typing_draft_ttl?: number;
|
||||||
contact_note_length_limit?: number;
|
contact_note_length_limit?: number;
|
||||||
|
settings_display_passkeys?: boolean;
|
||||||
|
passkeys_account_passkeys_max?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||||
@ -243,6 +245,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
verifyAgeCountry: appConfig.verify_age_country,
|
verifyAgeCountry: appConfig.verify_age_country,
|
||||||
verifyAgeMin: appConfig.verify_age_min,
|
verifyAgeMin: appConfig.verify_age_min,
|
||||||
typingDraftTtl: appConfig.message_typing_draft_ttl,
|
typingDraftTtl: appConfig.message_typing_draft_ttl,
|
||||||
|
arePasskeysAvailable: appConfig.settings_display_passkeys,
|
||||||
|
passkeysMaxCount: appConfig.passkeys_account_passkeys_max,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
ApiCountry,
|
ApiCountry,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
ApiOldLangString,
|
ApiOldLangString,
|
||||||
|
ApiPasskey,
|
||||||
ApiPendingSuggestion,
|
ApiPendingSuggestion,
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
ApiPromoData,
|
ApiPromoData,
|
||||||
@ -349,3 +350,14 @@ export function buildApiRestrictionReasons(
|
|||||||
{ reason, text, platform }) =>
|
{ reason, text, platform }) =>
|
||||||
({ reason, text, platform }));
|
({ reason, text, platform }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiPasskey(passkey: GramJs.TypePasskey): ApiPasskey {
|
||||||
|
const { id, name, date, softwareEmojiId, lastUsageDate } = passkey;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
date,
|
||||||
|
softwareEmojiId: softwareEmojiId?.toString(),
|
||||||
|
lastUsageDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -362,7 +362,7 @@ export function buildInputStory(story: ApiStory | ApiStorySkipped) {
|
|||||||
export function generateRandomTimestampedBigInt() {
|
export function generateRandomTimestampedBigInt() {
|
||||||
// 32 bits for timestamp, 32 bits are random
|
// 32 bits for timestamp, 32 bits are random
|
||||||
const buffer = generateRandomBytes(8);
|
const buffer = generateRandomBytes(8);
|
||||||
const timestampBuffer = Buffer.alloc(4);
|
const timestampBuffer = Buffer.allocUnsafe(4);
|
||||||
timestampBuffer.writeUInt32LE(Math.floor(Date.now() / 1000), 0);
|
timestampBuffer.writeUInt32LE(Math.floor(Date.now() / 1000), 0);
|
||||||
buffer.set(timestampBuffer, 4);
|
buffer.set(timestampBuffer, 4);
|
||||||
return readBigIntFromBuffer(buffer, true, true);
|
return readBigIntFromBuffer(buffer, true, true);
|
||||||
|
|||||||
31
src/api/gramjs/gramjsBuilders/passkeys.ts
Normal file
31
src/api/gramjs/gramjsBuilders/passkeys.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
|
import { base64UrlToBuffer, base64UrlToString } from '../../../util/encoding/base64';
|
||||||
|
|
||||||
|
export function buildInputPasskeyCredential(
|
||||||
|
credentialJson: PublicKeyCredentialJSON,
|
||||||
|
): GramJs.TypeInputPasskeyCredential {
|
||||||
|
let response: GramJs.TypeInputPasskeyResponse;
|
||||||
|
const clientData = base64UrlToString(credentialJson.response.clientDataJSON);
|
||||||
|
if (credentialJson.response.attestationObject) {
|
||||||
|
response = new GramJs.InputPasskeyResponseRegister({
|
||||||
|
clientData: new GramJs.DataJSON({ data: clientData }),
|
||||||
|
attestationData: base64UrlToBuffer(credentialJson.response.attestationObject),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const userHandle = base64UrlToString(credentialJson.response.userHandle);
|
||||||
|
|
||||||
|
response = new GramJs.InputPasskeyResponseLogin({
|
||||||
|
clientData: new GramJs.DataJSON({ data: clientData }),
|
||||||
|
authenticatorData: base64UrlToBuffer(credentialJson.response.authenticatorData),
|
||||||
|
signature: base64UrlToBuffer(credentialJson.response.signature),
|
||||||
|
userHandle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GramJs.InputPasskeyCredentialPublicKey({
|
||||||
|
id: credentialJson.id,
|
||||||
|
rawId: credentialJson.rawId,
|
||||||
|
response,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ const ERROR_KEYS: Record<string, RegularLangKey> = {
|
|||||||
SRP_PASSWORD_CHANGED: 'ErrorPasswordChanged',
|
SRP_PASSWORD_CHANGED: 'ErrorPasswordChanged',
|
||||||
CODE_INVALID: 'ErrorEmailCodeInvalid',
|
CODE_INVALID: 'ErrorEmailCodeInvalid',
|
||||||
PASSWORD_MISSING: 'ErrorPasswordMissing',
|
PASSWORD_MISSING: 'ErrorPasswordMissing',
|
||||||
|
PASSKEY_CREDENTIAL_NOT_FOUND: 'ErrorPasskeyUnknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageRepairContext = Pick<GramJs.TypeMessage, 'peerId' | 'id'>;
|
export type MessageRepairContext = Pick<GramJs.TypeMessage, 'peerId' | 'id'>;
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
import { PasskeyLoginRequestedError, UserAlreadyAuthorizedError } from '../../../lib/gramjs/errors';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ApiPasskeyOption,
|
||||||
ApiUpdateAuthorizationState,
|
ApiUpdateAuthorizationState,
|
||||||
ApiUpdateAuthorizationStateType,
|
ApiUpdateAuthorizationStateType,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
@ -19,6 +22,13 @@ export function onWebAuthTokenFailed() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onPasskeyOption(option: ApiPasskeyOption) {
|
||||||
|
sendApiUpdate({
|
||||||
|
'@type': 'updatePasskeyOption',
|
||||||
|
option,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function onRequestPhoneNumber() {
|
export function onRequestPhoneNumber() {
|
||||||
sendApiUpdate(buildAuthStateUpdate('authorizationStateWaitPhoneNumber'));
|
sendApiUpdate(buildAuthStateUpdate('authorizationStateWaitPhoneNumber'));
|
||||||
|
|
||||||
@ -75,11 +85,20 @@ export function onRequestQrCode(qrCode: { token: Buffer; expires: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function onAuthError(err: Error) {
|
export function onAuthError(err: Error) {
|
||||||
const { messageKey } = wrapError(err);
|
if (err instanceof UserAlreadyAuthorizedError) {
|
||||||
|
sendApiUpdate({
|
||||||
|
'@type': 'updateUserAlreadyAuthorized',
|
||||||
|
userId: err.userId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageKey, errorMessage } = wrapError(err);
|
||||||
|
|
||||||
sendApiUpdate({
|
sendApiUpdate({
|
||||||
'@type': 'updateAuthorizationError',
|
'@type': 'updateAuthorizationError',
|
||||||
errorKey: messageKey,
|
errorKey: messageKey,
|
||||||
|
errorCode: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,3 +170,11 @@ export function restartAuthWithQr() {
|
|||||||
|
|
||||||
authController.reject(new Error('RESTART_AUTH_WITH_QR'));
|
authController.reject(new Error('RESTART_AUTH_WITH_QR'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function restartAuthWithPasskey(credentialJson: PublicKeyCredentialJSON) {
|
||||||
|
if (!authController.reject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authController.reject(new PasskeyLoginRequestedError(credentialJson));
|
||||||
|
}
|
||||||
|
|||||||
@ -54,8 +54,16 @@ import {
|
|||||||
updateChannelState,
|
updateChannelState,
|
||||||
} from '../updates/updateManager';
|
} from '../updates/updateManager';
|
||||||
import {
|
import {
|
||||||
onAuthError, onAuthReady, onCurrentUserUpdate, onRequestCode, onRequestPassword, onRequestPhoneNumber,
|
onAuthError,
|
||||||
onRequestQrCode, onRequestRegistration, onWebAuthTokenFailed,
|
onAuthReady,
|
||||||
|
onCurrentUserUpdate,
|
||||||
|
onPasskeyOption,
|
||||||
|
onRequestCode,
|
||||||
|
onRequestPassword,
|
||||||
|
onRequestPhoneNumber,
|
||||||
|
onRequestQrCode,
|
||||||
|
onRequestRegistration,
|
||||||
|
onWebAuthTokenFailed,
|
||||||
} from './auth';
|
} from './auth';
|
||||||
import downloadMediaWithClient, { parseMediaUrl } from './media';
|
import downloadMediaWithClient, { parseMediaUrl } from './media';
|
||||||
|
|
||||||
@ -84,6 +92,7 @@ export async function init(initialArgs: ApiInitialArgs, onConnected?: NoneToVoid
|
|||||||
userAgent, platform, sessionData, isWebmSupported, maxBufferSize, webAuthToken, dcId,
|
userAgent, platform, sessionData, isWebmSupported, maxBufferSize, webAuthToken, dcId,
|
||||||
mockScenario, shouldForceHttpTransport, shouldAllowHttpTransport,
|
mockScenario, shouldForceHttpTransport, shouldAllowHttpTransport,
|
||||||
shouldDebugExportedSenders, langCode, isTestServerRequested, accountIds,
|
shouldDebugExportedSenders, langCode, isTestServerRequested, accountIds,
|
||||||
|
hasPasskeySupport,
|
||||||
} = initialArgs;
|
} = initialArgs;
|
||||||
|
|
||||||
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
||||||
@ -131,6 +140,7 @@ export async function init(initialArgs: ApiInitialArgs, onConnected?: NoneToVoid
|
|||||||
phoneCode: onRequestCode,
|
phoneCode: onRequestCode,
|
||||||
password: onRequestPassword,
|
password: onRequestPassword,
|
||||||
firstAndLastNames: onRequestRegistration,
|
firstAndLastNames: onRequestRegistration,
|
||||||
|
onPasskeyOption,
|
||||||
qrCode: onRequestQrCode,
|
qrCode: onRequestQrCode,
|
||||||
onError: onAuthError,
|
onError: onAuthError,
|
||||||
initialMethod: platform === 'iOS' || platform === 'Android' ? 'phoneNumber' : 'qrCode',
|
initialMethod: platform === 'iOS' || platform === 'Android' ? 'phoneNumber' : 'qrCode',
|
||||||
@ -139,6 +149,7 @@ export async function init(initialArgs: ApiInitialArgs, onConnected?: NoneToVoid
|
|||||||
webAuthTokenFailed: onWebAuthTokenFailed,
|
webAuthTokenFailed: onWebAuthTokenFailed,
|
||||||
mockScenario,
|
mockScenario,
|
||||||
accountIds,
|
accountIds,
|
||||||
|
hasPasskeySupport,
|
||||||
}, onConnected);
|
}, onConnected);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr,
|
provideAuthPhoneNumber, provideAuthCode, provideAuthPassword, provideAuthRegistration, restartAuth, restartAuthWithQr,
|
||||||
|
restartAuthWithPasskey,
|
||||||
} from './auth';
|
} from './auth';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
ApiInputPrivacyRules,
|
ApiInputPrivacyRules,
|
||||||
ApiLanguage,
|
ApiLanguage,
|
||||||
ApiNotifyPeerType,
|
ApiNotifyPeerType,
|
||||||
|
ApiPasskeyRegistrationOption,
|
||||||
ApiPeerNotifySettings,
|
ApiPeerNotifySettings,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
@ -16,6 +17,7 @@ import type {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ACCEPTABLE_USERNAME_ERRORS,
|
ACCEPTABLE_USERNAME_ERRORS,
|
||||||
|
DEBUG,
|
||||||
LANG_PACK,
|
LANG_PACK,
|
||||||
MUTE_INDEFINITE_TIMESTAMP,
|
MUTE_INDEFINITE_TIMESTAMP,
|
||||||
UNMUTE_TIMESTAMP,
|
UNMUTE_TIMESTAMP,
|
||||||
@ -28,6 +30,7 @@ import { buildApiDisallowedGiftsSettings } from '../apiBuilders/gifts';
|
|||||||
import {
|
import {
|
||||||
buildApiCountryList,
|
buildApiCountryList,
|
||||||
buildApiLanguage,
|
buildApiLanguage,
|
||||||
|
buildApiPasskey,
|
||||||
buildApiSession,
|
buildApiSession,
|
||||||
buildApiTimezone,
|
buildApiTimezone,
|
||||||
buildApiWallpaper,
|
buildApiWallpaper,
|
||||||
@ -44,12 +47,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildDisallowedGiftsSettings,
|
buildDisallowedGiftsSettings,
|
||||||
buildInputChannel,
|
buildInputChannel,
|
||||||
buildInputPeer, buildInputPhoto,
|
buildInputPeer,
|
||||||
|
buildInputPhoto,
|
||||||
buildInputPrivacyKey,
|
buildInputPrivacyKey,
|
||||||
buildInputPrivacyRules,
|
buildInputPrivacyRules,
|
||||||
buildInputUser,
|
buildInputUser,
|
||||||
DEFAULT_PRIMITIVES,
|
DEFAULT_PRIMITIVES,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
|
import { buildInputPasskeyCredential } from '../gramjsBuilders/passkeys';
|
||||||
import { addPhotoToLocalDb } from '../helpers/localDb';
|
import { addPhotoToLocalDb } from '../helpers/localDb';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { getClient, invokeRequest, uploadFile } from './client';
|
import { getClient, invokeRequest, uploadFile } from './client';
|
||||||
@ -766,3 +771,48 @@ export function reorderUsernames({ chatId, accessHash, usernames }: {
|
|||||||
order: usernames,
|
order: usernames,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchPasskeys() {
|
||||||
|
const result = await invokeRequest(new GramJs.account.GetPasskeys());
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
passkeys: result.passkeys.map(buildApiPasskey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initPasskeyRegistration() {
|
||||||
|
const result = await invokeRequest(new GramJs.account.InitPasskeyRegistration());
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(result.options.data) as ApiPasskeyRegistrationOption;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Failed to parse passkey registration options:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerPasskey(credentialJson: PublicKeyCredentialJSON) {
|
||||||
|
const result = await invokeRequest(new GramJs.account.RegisterPasskey({
|
||||||
|
credential: buildInputPasskeyCredential(credentialJson),
|
||||||
|
}));
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildApiPasskey(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePasskey({ id }: { id: string }) {
|
||||||
|
return invokeRequest(new GramJs.account.DeletePasskey({ id }), {
|
||||||
|
shouldReturnTrue: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -25,8 +25,17 @@ export interface ApiInitialArgs {
|
|||||||
langCode: string;
|
langCode: string;
|
||||||
isTestServerRequested?: boolean;
|
isTestServerRequested?: boolean;
|
||||||
accountIds?: string[];
|
accountIds?: string[];
|
||||||
|
hasPasskeySupport?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiPasskeyOption = {
|
||||||
|
publicKey: PublicKeyCredentialRequestOptionsJSON;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiPasskeyRegistrationOption = {
|
||||||
|
publicKey: PublicKeyCredentialCreationOptionsJSON;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ApiOnProgress {
|
export interface ApiOnProgress {
|
||||||
(
|
(
|
||||||
progress: number, // Float between 0 and 1.
|
progress: number, // Float between 0 and 1.
|
||||||
@ -276,6 +285,8 @@ export interface ApiAppConfig {
|
|||||||
verifyAgeMin?: number;
|
verifyAgeMin?: number;
|
||||||
typingDraftTtl: number;
|
typingDraftTtl: number;
|
||||||
contactNoteLimit?: number;
|
contactNoteLimit?: number;
|
||||||
|
arePasskeysAvailable: boolean;
|
||||||
|
passkeysMaxCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiConfig {
|
export interface ApiConfig {
|
||||||
@ -390,3 +401,11 @@ export interface ApiRestrictionReason {
|
|||||||
text: string;
|
text: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiPasskey {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
date: number;
|
||||||
|
softwareEmojiId?: string;
|
||||||
|
lastUsageDate?: number;
|
||||||
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import type {
|
|||||||
ApiEmojiInteraction,
|
ApiEmojiInteraction,
|
||||||
ApiError,
|
ApiError,
|
||||||
ApiNotifyPeerType,
|
ApiNotifyPeerType,
|
||||||
|
ApiPasskeyOption,
|
||||||
ApiPeerNotifySettings,
|
ApiPeerNotifySettings,
|
||||||
ApiSessionData,
|
ApiSessionData,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
@ -85,6 +86,11 @@ export type ApiUpdateWebAuthTokenFailed = {
|
|||||||
'@type': 'updateWebAuthTokenFailed';
|
'@type': 'updateWebAuthTokenFailed';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePasskeyOption = {
|
||||||
|
'@type': 'updatePasskeyOption';
|
||||||
|
option: ApiPasskeyOption;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdateSession = {
|
export type ApiUpdateSession = {
|
||||||
'@type': 'updateSession';
|
'@type': 'updateSession';
|
||||||
sessionData?: ApiSessionData;
|
sessionData?: ApiSessionData;
|
||||||
@ -93,6 +99,12 @@ export type ApiUpdateSession = {
|
|||||||
export type ApiUpdateAuthorizationError = {
|
export type ApiUpdateAuthorizationError = {
|
||||||
'@type': 'updateAuthorizationError';
|
'@type': 'updateAuthorizationError';
|
||||||
errorKey: RegularLangFnParameters;
|
errorKey: RegularLangFnParameters;
|
||||||
|
errorCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateUserAlreadyAuthorized = {
|
||||||
|
'@type': 'updateUserAlreadyAuthorized';
|
||||||
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiUpdateConnectionState = {
|
export type ApiUpdateConnectionState = {
|
||||||
@ -877,11 +889,11 @@ export type ApiUpdate = (
|
|||||||
ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
|
ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
|
||||||
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
|
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
|
||||||
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
|
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
|
||||||
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages |
|
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption |
|
||||||
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
|
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
|
||||||
ApiDeleteParticipantHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
|
ApiDeleteParticipantHistory | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
|
||||||
ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus |
|
ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus |
|
||||||
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending | ApiUpdatePeerSettings |
|
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending | ApiUpdatePeerSettings | ApiUpdateUserAlreadyAuthorized |
|
||||||
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateScheduledMessageSendFailed |
|
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateScheduledMessageSendFailed |
|
||||||
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
|
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
|
||||||
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
||||||
|
|||||||
@ -175,6 +175,7 @@
|
|||||||
"LoginQRHelp2" = "Go to **Settings** > **Devices** > **Link Desktop Device**";
|
"LoginQRHelp2" = "Go to **Settings** > **Devices** > **Link Desktop Device**";
|
||||||
"LoginQRHelp3" = "Point your phone at this screen to confirm login";
|
"LoginQRHelp3" = "Point your phone at this screen to confirm login";
|
||||||
"LoginQRCancel" = "Log in by phone number";
|
"LoginQRCancel" = "Log in by phone number";
|
||||||
|
"LoginPasskey" = "Log in with Passkey";
|
||||||
"YourName" = "Your Name";
|
"YourName" = "Your Name";
|
||||||
"LoginRegisterDesc" = "Enter your name and add a profile photo.";
|
"LoginRegisterDesc" = "Enter your name and add a profile photo.";
|
||||||
"LoginRegisterFirstNamePlaceholder" = "First Name";
|
"LoginRegisterFirstNamePlaceholder" = "First Name";
|
||||||
@ -519,8 +520,6 @@
|
|||||||
"Users_one" = "{count} user";
|
"Users_one" = "{count} user";
|
||||||
"Users_other" = "{count} users";
|
"Users_other" = "{count} users";
|
||||||
"PrivacySettingsWebSessions" = "Active Websites";
|
"PrivacySettingsWebSessions" = "Active Websites";
|
||||||
"PasswordOn" = "On";
|
|
||||||
"PasswordOff" = "Off";
|
|
||||||
"PrivacyTitle" = "Privacy";
|
"PrivacyTitle" = "Privacy";
|
||||||
"PrivacyPhoneTitle" = "Who can see my phone number?";
|
"PrivacyPhoneTitle" = "Who can see my phone number?";
|
||||||
"LastSeenTitle" = "Who can see your Last Seen time?";
|
"LastSeenTitle" = "Who can see your Last Seen time?";
|
||||||
@ -717,6 +716,7 @@
|
|||||||
"ErrorNewSaltInvalid" = "Error validating password, please try again";
|
"ErrorNewSaltInvalid" = "Error validating password, please try again";
|
||||||
"ErrorPasswordChanged" = "Password has been changed, please try again";
|
"ErrorPasswordChanged" = "Password has been changed, please try again";
|
||||||
"ErrorPasswordMissing" = "You must set 2FA password to use this feature";
|
"ErrorPasswordMissing" = "You must set 2FA password to use this feature";
|
||||||
|
"ErrorPasskeyUnknown" = "This passkey is not assigned to any account";
|
||||||
"ErrorUnspecified" = "Error";
|
"ErrorUnspecified" = "Error";
|
||||||
"NoStickers" = "No stickers yet";
|
"NoStickers" = "No stickers yet";
|
||||||
"ClearRecentEmoji" = "Clear recent emoji?";
|
"ClearRecentEmoji" = "Clear recent emoji?";
|
||||||
@ -2406,6 +2406,29 @@
|
|||||||
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
|
"StarGiftUpgradeCostModalTitle" = "Upgrade Cost";
|
||||||
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
"StarGiftUpgradeCostHint" = "Users who upgrade their gifts first get collectibles with shorter numbers.";
|
||||||
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";
|
"StarGiftPriceDecreaseTimer" = "Price decreases in {timer}";
|
||||||
|
"SettingsItemPrivacyPasskeys" = "Passkeys";
|
||||||
|
"SettingsItemPrivacyOn" = "Enabled";
|
||||||
|
"SettingsItemPrivacyOff" = "Disabled";
|
||||||
|
"SettingsPasskeyUsedAt" = "Last used {date}";
|
||||||
|
"SettingsPasskeyTitle" = "Passkeys";
|
||||||
|
"SettingsPasskeyInfo" = "Manage your passkey, stored safely in the cloud service you choose.";
|
||||||
|
"SettingsPasskeyFallbackTitle" = "Passkey";
|
||||||
|
"SettingsPasskeysFooter" = "Your passkeys are stored securely in your password manager. {link}";
|
||||||
|
"SettingsPasskeysFooterLink" = "Learn more >";
|
||||||
|
"SettingsPasskeysCreate" = "Create a Passkey";
|
||||||
|
"PasskeyModalTitle" = "Protect your account";
|
||||||
|
"PasskeyModalDescription" = "Log in safely and keep your account secure.";
|
||||||
|
"PasskeyModalFeature1Title" = "Create a Passkey";
|
||||||
|
"PasskeyModalFeature1Description" = "Make a passkey to sign in easily and safely.";
|
||||||
|
"PasskeyModalFeature2Title" = "Log in with face recognition";
|
||||||
|
"PasskeyModalFeature2Description" = "Use your face, fingerprint, or passcode to sign in.";
|
||||||
|
"PasskeyModalFeature3Title" = "Store Passkey securely";
|
||||||
|
"PasskeyModalFeature3Description" = "Your passkey is stored safely in the cloud service you choose.";
|
||||||
|
"PasskeyModalButtonText" = "Create Passkey";
|
||||||
|
"PasskeyDeleteTitle" = "Delete Passkey";
|
||||||
|
"PasskeyDeleteText" = "Once deleted, this passkey can't be used to log in.\nDon't forget to remove it from your password manager.";
|
||||||
|
"PasskeyCreateError" = "Failed to create passkey. Please try again.";
|
||||||
|
"PasskeyLoginError" = "Failed to log in with a passkey";
|
||||||
"UnconfirmedAuthDeniedTitle" = "New Login Prevented";
|
"UnconfirmedAuthDeniedTitle" = "New Login Prevented";
|
||||||
"UnconfirmedAuthDeniedMessage" = "We have terminated the login attempt from {location}.";
|
"UnconfirmedAuthDeniedMessage" = "We have terminated the login attempt from {location}.";
|
||||||
"UnconfirmedAuthTitle" = "Someone just got access to your messages!";
|
"UnconfirmedAuthTitle" = "Someone just got access to your messages!";
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const INITIAL_KEYS: LangKey[] = [
|
|||||||
'LoginQRHelp2',
|
'LoginQRHelp2',
|
||||||
'LoginQRHelp3',
|
'LoginQRHelp3',
|
||||||
'LoginQRCancel',
|
'LoginQRCancel',
|
||||||
|
'LoginPasskey',
|
||||||
'YourName',
|
'YourName',
|
||||||
'LoginRegisterDesc',
|
'LoginRegisterDesc',
|
||||||
'LoginRegisterFirstNamePlaceholder',
|
'LoginRegisterFirstNamePlaceholder',
|
||||||
|
|||||||
@ -18,7 +18,8 @@ export default {
|
|||||||
"LoginQRHelp1": "Open Telegram on your phone",
|
"LoginQRHelp1": "Open Telegram on your phone",
|
||||||
"LoginQRHelp2": "Go to **Settings** > **Devices** > **Link Desktop Device**",
|
"LoginQRHelp2": "Go to **Settings** > **Devices** > **Link Desktop Device**",
|
||||||
"LoginQRHelp3": "Point your phone at this screen to confirm login",
|
"LoginQRHelp3": "Point your phone at this screen to confirm login",
|
||||||
"LoginQRCancel": "Log in by phone Number",
|
"LoginQRCancel": "Log in by phone number",
|
||||||
|
"LoginPasskey": "Log in with Passkey",
|
||||||
"YourName": "Your Name",
|
"YourName": "Your Name",
|
||||||
"LoginRegisterDesc": "Enter your name and add a profile photo.",
|
"LoginRegisterDesc": "Enter your name and add a profile photo.",
|
||||||
"LoginRegisterFirstNamePlaceholder": "First Name",
|
"LoginRegisterFirstNamePlaceholder": "First Name",
|
||||||
@ -26,12 +27,12 @@ export default {
|
|||||||
"LoginSelectCountryTitle": "Country",
|
"LoginSelectCountryTitle": "Country",
|
||||||
"CountryNone": "Country not found",
|
"CountryNone": "Country not found",
|
||||||
"PleaseEnterPassword": "Enter your new password",
|
"PleaseEnterPassword": "Enter your new password",
|
||||||
"ErrorPhoneNumberInvalid": "Invalid phone number, please try again.",
|
"ErrorPhoneNumberInvalid": "Invalid phone number, please try again",
|
||||||
"ErrorCodeInvalid": "Invalid code, please try again.",
|
"ErrorCodeInvalid": "Invalid code, please try again",
|
||||||
"ErrorIncorrectPassword": "Invalid password, please try again.",
|
"ErrorIncorrectPassword": "Invalid password, please try again",
|
||||||
"ErrorPasswordFlood": "Too many attempts, please try again later.",
|
"ErrorPasswordFlood": "Too many attempts, please try again later",
|
||||||
"ErrorPhoneBanned": "This phone number is banned.",
|
"ErrorPhoneBanned": "This phone number is banned",
|
||||||
"ErrorFloodTime": "Too many attempts, please try again in {time}.",
|
"ErrorFloodTime": "Too many attempts, please try again in {time}",
|
||||||
"ErrorUnexpected": "Unexpected error",
|
"ErrorUnexpected": "Unexpected error",
|
||||||
"ErrorUnexpectedMessage": "Unexpected error: {error}"
|
"ErrorUnexpectedMessage": "Unexpected error: {error}"
|
||||||
} as Record<LangKey, LangPackStringValue>;
|
} as Record<LangKey, LangPackStringValue>;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
1
src/assets/tgs-previews/settings/Passkeys.svg
Normal file
1
src/assets/tgs-previews/settings/Passkeys.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/tgs/settings/Passkeys.tgs
Normal file
BIN
src/assets/tgs/settings/Passkeys.tgs
Normal file
Binary file not shown.
@ -5,7 +5,6 @@ export { default as ForwardRecipientPicker } from '../components/main/ForwardRec
|
|||||||
export { default as DraftRecipientPicker } from '../components/main/DraftRecipientPicker';
|
export { default as DraftRecipientPicker } from '../components/main/DraftRecipientPicker';
|
||||||
export { default as AttachBotRecipientPicker } from '../components/main/AttachBotRecipientPicker';
|
export { default as AttachBotRecipientPicker } from '../components/main/AttachBotRecipientPicker';
|
||||||
export { default as Dialogs } from '../components/main/Dialogs';
|
export { default as Dialogs } from '../components/main/Dialogs';
|
||||||
export { default as Notifications } from '../components/main/Notifications';
|
|
||||||
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
|
export { default as SafeLinkModal } from '../components/main/SafeLinkModal';
|
||||||
export { default as MapModal } from '../components/modals/map/MapModal';
|
export { default as MapModal } from '../components/modals/map/MapModal';
|
||||||
export { default as UrlAuthModal } from '../components/modals/urlAuth/UrlAuthModal';
|
export { default as UrlAuthModal } from '../components/modals/urlAuth/UrlAuthModal';
|
||||||
@ -27,6 +26,7 @@ export { default as DeleteAccountModal } from '../components/modals/deleteAccoun
|
|||||||
export { default as AgeVerificationModal } from '../components/modals/ageVerification/AgeVerificationModal';
|
export { default as AgeVerificationModal } from '../components/modals/ageVerification/AgeVerificationModal';
|
||||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||||
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
|
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
|
||||||
|
export { default as PasskeyModal } from '../components/modals/passkey/PasskeyModal';
|
||||||
export { default as BirthdaySetupModal } from '../components/modals/birthday/BirthdaySetupModal';
|
export { default as BirthdaySetupModal } from '../components/modals/birthday/BirthdaySetupModal';
|
||||||
|
|
||||||
export { default as AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal';
|
export { default as AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal';
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../lib/teact/teact';
|
|
||||||
import { useEffect, useLayoutEffect } from '../lib/teact/teact';
|
import { useEffect, useLayoutEffect } from '../lib/teact/teact';
|
||||||
import { withGlobal } from '../global';
|
import { withGlobal } from '../global';
|
||||||
|
|
||||||
@ -22,11 +21,12 @@ import { updateSizes } from '../util/windowSize';
|
|||||||
import useTauriDrag from '../hooks/tauri/useTauriDrag';
|
import useTauriDrag from '../hooks/tauri/useTauriDrag';
|
||||||
import useAppLayout from '../hooks/useAppLayout';
|
import useAppLayout from '../hooks/useAppLayout';
|
||||||
import usePrevious from '../hooks/usePrevious';
|
import usePrevious from '../hooks/usePrevious';
|
||||||
import { useSignalEffect } from '../hooks/useSignalEffect.ts';
|
import { useSignalEffect } from '../hooks/useSignalEffect';
|
||||||
import { getIsInBackground } from '../hooks/window/useBackgroundMode.ts';
|
import { getIsInBackground } from '../hooks/window/useBackgroundMode';
|
||||||
|
|
||||||
// import Test from './test/TestLocale';
|
// import Test from './test/TestLocale';
|
||||||
import Auth from './auth/Auth';
|
import Auth from './auth/Auth';
|
||||||
|
import Notifications from './common/Notifications';
|
||||||
import UiLoader from './common/UiLoader';
|
import UiLoader from './common/UiLoader';
|
||||||
import AppInactive from './main/AppInactive';
|
import AppInactive from './main/AppInactive';
|
||||||
import LockScreen from './main/LockScreen.async';
|
import LockScreen from './main/LockScreen.async';
|
||||||
@ -36,7 +36,7 @@ import Transition from './ui/Transition';
|
|||||||
import styles from './App.module.scss';
|
import styles from './App.module.scss';
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
authState: GlobalState['authState'];
|
authState: GlobalState['auth']['state'];
|
||||||
isScreenLocked?: boolean;
|
isScreenLocked?: boolean;
|
||||||
hasPasscode?: boolean;
|
hasPasscode?: boolean;
|
||||||
inactiveReason?: 'auth' | 'otherClient';
|
inactiveReason?: 'auth' | 'otherClient';
|
||||||
@ -57,7 +57,7 @@ const TRANSITION_RENDER_COUNT = Object.keys(AppScreens).length / 2;
|
|||||||
const ACTIVE_PAGE_TITLE = IS_TAURI ? PAGE_TITLE_TAURI : PAGE_TITLE;
|
const ACTIVE_PAGE_TITLE = IS_TAURI ? PAGE_TITLE_TAURI : PAGE_TITLE;
|
||||||
const INACTIVE_PAGE_TITLE = `${ACTIVE_PAGE_TITLE} ${INACTIVE_MARKER}`;
|
const INACTIVE_PAGE_TITLE = `${ACTIVE_PAGE_TITLE} ${INACTIVE_MARKER}`;
|
||||||
|
|
||||||
const App: FC<StateProps> = ({
|
const App = ({
|
||||||
authState,
|
authState,
|
||||||
isScreenLocked,
|
isScreenLocked,
|
||||||
hasPasscode,
|
hasPasscode,
|
||||||
@ -66,7 +66,7 @@ const App: FC<StateProps> = ({
|
|||||||
isTestServer,
|
isTestServer,
|
||||||
theme,
|
theme,
|
||||||
actionMessageBg,
|
actionMessageBg,
|
||||||
}) => {
|
}: StateProps) => {
|
||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
|
||||||
|
|
||||||
@ -257,18 +257,20 @@ const App: FC<StateProps> = ({
|
|||||||
{renderContent}
|
{renderContent}
|
||||||
</Transition>
|
</Transition>
|
||||||
{activeKey === AppScreens.auth && isTestServer && <div className="test-server-badge">Test server</div>}
|
{activeKey === AppScreens.auth && isTestServer && <div className="test-server-badge">Test server</div>}
|
||||||
|
<Notifications />
|
||||||
</UiLoader>
|
</UiLoader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withGlobal(
|
export default withGlobal(
|
||||||
(global): Complete<StateProps> => {
|
(global): Complete<StateProps> => {
|
||||||
|
const { state: authState, hasWebAuthTokenFailed, hasWebAuthTokenPasswordRequired } = global.auth;
|
||||||
return {
|
return {
|
||||||
authState: global.authState,
|
authState,
|
||||||
isScreenLocked: global.passcode?.isScreenLocked,
|
isScreenLocked: global.passcode?.isScreenLocked,
|
||||||
hasPasscode: global.passcode?.hasPasscode,
|
hasPasscode: global.passcode?.hasPasscode,
|
||||||
inactiveReason: selectTabState(global).inactiveReason,
|
inactiveReason: selectTabState(global).inactiveReason,
|
||||||
hasWebAuthTokenFailed: global.hasWebAuthTokenFailed || global.hasWebAuthTokenPasswordRequired,
|
hasWebAuthTokenFailed: hasWebAuthTokenFailed || hasWebAuthTokenPasswordRequired,
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
isTestServer: global.config?.isTestServer,
|
isTestServer: global.config?.isTestServer,
|
||||||
actionMessageBg: selectActionMessageBg(global),
|
actionMessageBg: selectActionMessageBg(global),
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import '../../global/actions/initial';
|
import '../../global/actions/initial';
|
||||||
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -21,11 +20,13 @@ import AuthRegister from './AuthRegister.async';
|
|||||||
|
|
||||||
import './Auth.scss';
|
import './Auth.scss';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authState'>;
|
type StateProps = {
|
||||||
|
authState: GlobalState['auth']['state'];
|
||||||
|
};
|
||||||
|
|
||||||
const Auth: FC<StateProps> = ({
|
const Auth = ({
|
||||||
authState,
|
authState,
|
||||||
}) => {
|
}: StateProps) => {
|
||||||
const {
|
const {
|
||||||
returnToAuthPhoneNumber, goToAuthQrCode,
|
returnToAuthPhoneNumber, goToAuthQrCode,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
@ -101,7 +102,7 @@ const Auth: FC<StateProps> = ({
|
|||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => {
|
(global): Complete<StateProps> => {
|
||||||
return {
|
return {
|
||||||
authState: global.authState,
|
authState: global.auth.state,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Auth));
|
)(Auth));
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import type { FormEvent } from 'react';
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import {
|
import {
|
||||||
memo, useCallback, useEffect, useRef, useState,
|
memo, useCallback, useEffect, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
@ -18,22 +16,23 @@ import TrackingMonkey from '../common/TrackingMonkey';
|
|||||||
import InputText from '../ui/InputText';
|
import InputText from '../ui/InputText';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authPhoneNumber' | 'authIsCodeViaApp' | 'authIsLoading' | 'authErrorKey'>;
|
type StateProps = {
|
||||||
|
auth: GlobalState['auth'];
|
||||||
|
};
|
||||||
|
|
||||||
const CODE_LENGTH = 5;
|
const CODE_LENGTH = 5;
|
||||||
|
|
||||||
const AuthCode: FC<StateProps> = ({
|
const AuthCode = ({
|
||||||
authPhoneNumber,
|
auth,
|
||||||
authIsCodeViaApp,
|
}: StateProps) => {
|
||||||
authIsLoading,
|
|
||||||
authErrorKey,
|
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
setAuthCode,
|
setAuthCode,
|
||||||
returnToAuthPhoneNumber,
|
returnToAuthPhoneNumber,
|
||||||
clearAuthErrorKey,
|
clearAuthErrorKey,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const { phoneNumber, isCodeViaApp, isLoading, errorKey } = auth;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
const inputRef = useRef<HTMLInputElement>();
|
||||||
|
|
||||||
@ -52,8 +51,8 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
onBack: returnToAuthPhoneNumber,
|
onBack: returnToAuthPhoneNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onCodeChange = useCallback((e: FormEvent<HTMLInputElement>) => {
|
const onCodeChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {
|
||||||
if (authErrorKey) {
|
if (errorKey) {
|
||||||
clearAuthErrorKey();
|
clearAuthErrorKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +80,7 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
if (target.value.length === CODE_LENGTH) {
|
if (target.value.length === CODE_LENGTH) {
|
||||||
setAuthCode({ code: target.value });
|
setAuthCode({ code: target.value });
|
||||||
}
|
}
|
||||||
}, [authErrorKey, code, isTracking, setAuthCode]);
|
}, [errorKey, code, isTracking]);
|
||||||
|
|
||||||
function handleReturnToAuthPhoneNumber() {
|
function handleReturnToAuthPhoneNumber() {
|
||||||
returnToAuthPhoneNumber();
|
returnToAuthPhoneNumber();
|
||||||
@ -97,7 +96,7 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
trackingDirection={trackingDirection}
|
trackingDirection={trackingDirection}
|
||||||
/>
|
/>
|
||||||
<h1>
|
<h1>
|
||||||
{authPhoneNumber}
|
{phoneNumber}
|
||||||
<div
|
<div
|
||||||
className="auth-number-edit div-button"
|
className="auth-number-edit div-button"
|
||||||
onClick={handleReturnToAuthPhoneNumber}
|
onClick={handleReturnToAuthPhoneNumber}
|
||||||
@ -110,7 +109,7 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="note">
|
<p className="note">
|
||||||
{lang(authIsCodeViaApp ? 'SentAppCode' : 'LoginJustSentSms', undefined, {
|
{lang(isCodeViaApp ? 'SentAppCode' : 'LoginJustSentSms', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
withMarkdown: true,
|
withMarkdown: true,
|
||||||
})}
|
})}
|
||||||
@ -121,11 +120,11 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
label={lang('Code')}
|
label={lang('Code')}
|
||||||
onInput={onCodeChange}
|
onInput={onCodeChange}
|
||||||
value={code}
|
value={code}
|
||||||
error={authErrorKey && lang.withRegular(authErrorKey)}
|
error={errorKey && lang.withRegular(errorKey)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
/>
|
/>
|
||||||
{authIsLoading && <Loading />}
|
{isLoading && <Loading />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -133,6 +132,6 @@ const AuthCode: FC<StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => (
|
(global): Complete<StateProps> => (
|
||||||
pick(global, ['authPhoneNumber', 'authIsCodeViaApp', 'authIsLoading', 'authErrorKey']) as Complete<StateProps>
|
pick(global, ['auth'])
|
||||||
),
|
),
|
||||||
)(AuthCode));
|
)(AuthCode));
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import { memo, useCallback, useState } from '../../lib/teact/teact';
|
import { memo, useCallback, useState } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -11,12 +10,15 @@ import useLang from '../../hooks/useLang';
|
|||||||
import PasswordForm from '../common/PasswordForm';
|
import PasswordForm from '../common/PasswordForm';
|
||||||
import MonkeyPassword from '../common/PasswordMonkey';
|
import MonkeyPassword from '../common/PasswordMonkey';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authIsLoading' | 'authErrorKey' | 'authHint'>;
|
type StateProps = {
|
||||||
|
auth: GlobalState['auth'];
|
||||||
|
};
|
||||||
|
|
||||||
const AuthPassword: FC<StateProps> = ({
|
const AuthPassword = ({
|
||||||
authIsLoading, authErrorKey, authHint,
|
auth,
|
||||||
}) => {
|
}: StateProps) => {
|
||||||
const { setAuthPassword, clearAuthErrorKey } = getActions();
|
const { setAuthPassword, clearAuthErrorKey } = getActions();
|
||||||
|
const { isLoading, errorKey, hint } = auth;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
@ -37,9 +39,9 @@ const AuthPassword: FC<StateProps> = ({
|
|||||||
<p className="note">{lang('LoginEnterPasswordDescription')}</p>
|
<p className="note">{lang('LoginEnterPasswordDescription')}</p>
|
||||||
<PasswordForm
|
<PasswordForm
|
||||||
onClearError={clearAuthErrorKey}
|
onClearError={clearAuthErrorKey}
|
||||||
error={authErrorKey && lang.withRegular(authErrorKey)}
|
error={errorKey && lang.withRegular(errorKey)}
|
||||||
hint={authHint}
|
hint={hint}
|
||||||
isLoading={authIsLoading}
|
isLoading={isLoading}
|
||||||
isPasswordVisible={showPassword}
|
isPasswordVisible={showPassword}
|
||||||
onChangePasswordVisibility={handleChangePasswordVisibility}
|
onChangePasswordVisibility={handleChangePasswordVisibility}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
@ -51,6 +53,6 @@ const AuthPassword: FC<StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => (
|
(global): Complete<StateProps> => (
|
||||||
pick(global, ['authIsLoading', 'authErrorKey', 'authHint']) as Complete<StateProps>
|
pick(global, ['auth'])
|
||||||
),
|
),
|
||||||
)(AuthPassword));
|
)(AuthPassword));
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import type React from '../../lib/teact/teact';
|
|
||||||
import {
|
import {
|
||||||
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
@ -13,7 +10,6 @@ import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
|||||||
import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
|
import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
|
||||||
import { preloadImage } from '../../util/files';
|
import { preloadImage } from '../../util/files';
|
||||||
import preloadFonts from '../../util/fonts';
|
import preloadFonts from '../../util/fonts';
|
||||||
import { pick } from '../../util/iteratees';
|
|
||||||
import { getAccountSlotUrl } from '../../util/multiaccount';
|
import { getAccountSlotUrl } from '../../util/multiaccount';
|
||||||
import { oldSetLanguage } from '../../util/oldLangProvider';
|
import { oldSetLanguage } from '../../util/oldLangProvider';
|
||||||
import { formatPhoneNumber, getCountryCodeByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber';
|
import { formatPhoneNumber, getCountryCodeByIso, getCountryFromPhoneNumber } from '../../util/phoneNumber';
|
||||||
@ -34,12 +30,9 @@ import CountryCodeInput from './CountryCodeInput';
|
|||||||
|
|
||||||
import monkeyPath from '../../assets/monkey.svg';
|
import monkeyPath from '../../assets/monkey.svg';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, (
|
type StateProps = {
|
||||||
'connectionState' | 'authState' |
|
auth: GlobalState['auth'];
|
||||||
'authPhoneNumber' | 'authIsLoading' |
|
connectionState: GlobalState['connectionState'];
|
||||||
'authIsLoadingQrCode' | 'authErrorKey' |
|
|
||||||
'authRememberMe' | 'authNearestCountry'
|
|
||||||
)> & {
|
|
||||||
language?: string;
|
language?: string;
|
||||||
phoneCodeList: ApiCountryCode[];
|
phoneCodeList: ApiCountryCode[];
|
||||||
isTestServer?: boolean;
|
isTestServer?: boolean;
|
||||||
@ -49,19 +42,13 @@ const MIN_NUMBER_LENGTH = 7;
|
|||||||
|
|
||||||
let isPreloadInitiated = false;
|
let isPreloadInitiated = false;
|
||||||
|
|
||||||
const AuthPhoneNumber: FC<StateProps> = ({
|
const AuthPhoneNumber = ({
|
||||||
|
auth,
|
||||||
connectionState,
|
connectionState,
|
||||||
authState,
|
|
||||||
authPhoneNumber,
|
|
||||||
authIsLoading,
|
|
||||||
authIsLoadingQrCode,
|
|
||||||
authErrorKey,
|
|
||||||
authRememberMe,
|
|
||||||
authNearestCountry,
|
|
||||||
phoneCodeList,
|
phoneCodeList,
|
||||||
language,
|
language,
|
||||||
isTestServer,
|
isTestServer,
|
||||||
}) => {
|
}: StateProps) => {
|
||||||
const {
|
const {
|
||||||
setAuthPhoneNumber,
|
setAuthPhoneNumber,
|
||||||
setAuthRememberMe,
|
setAuthRememberMe,
|
||||||
@ -70,8 +57,20 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
clearAuthErrorKey,
|
clearAuthErrorKey,
|
||||||
goToAuthQrCode,
|
goToAuthQrCode,
|
||||||
setSharedSettingOption,
|
setSharedSettingOption,
|
||||||
|
loginWithPasskey,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
phoneNumber: authPhoneNumber,
|
||||||
|
nearestCountry,
|
||||||
|
isLoading: authIsLoading,
|
||||||
|
errorKey,
|
||||||
|
rememberMe,
|
||||||
|
isLoadingQrCode,
|
||||||
|
passkeyOption,
|
||||||
|
} = auth;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
const inputRef = useRef<HTMLInputElement>();
|
||||||
const suggestedLanguage = getSuggestedLanguage();
|
const suggestedLanguage = getSuggestedLanguage();
|
||||||
@ -105,10 +104,10 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
}, [country]);
|
}, [country]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && !authNearestCountry) {
|
if (isConnected && !nearestCountry) {
|
||||||
loadNearestCountry();
|
loadNearestCountry();
|
||||||
}
|
}
|
||||||
}, [isConnected, authNearestCountry]);
|
}, [isConnected, nearestCountry]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
@ -117,10 +116,10 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
}, [isConnected, language]);
|
}, [isConnected, language]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authNearestCountry && phoneCodeList && !country && !isTouched) {
|
if (nearestCountry && phoneCodeList && !country && !isTouched) {
|
||||||
setCountry(getCountryCodeByIso(phoneCodeList, authNearestCountry));
|
setCountry(getCountryCodeByIso(phoneCodeList, nearestCountry));
|
||||||
}
|
}
|
||||||
}, [country, authNearestCountry, isTouched, phoneCodeList]);
|
}, [country, nearestCountry, isTouched, phoneCodeList]);
|
||||||
|
|
||||||
const parseFullNumber = useLastCallback((newFullNumber: string) => {
|
const parseFullNumber = useLastCallback((newFullNumber: string) => {
|
||||||
if (!newFullNumber.length) {
|
if (!newFullNumber.length) {
|
||||||
@ -181,8 +180,8 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
setPhoneNumber('');
|
setPhoneNumber('');
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePhoneNumberChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const handlePhoneNumberChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (authErrorKey) {
|
if (errorKey) {
|
||||||
clearAuthErrorKey();
|
clearAuthErrorKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +208,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
parseFullNumber(shouldFixSafariAutoComplete ? `${country.countryCode} ${value}` : value);
|
parseFullNumber(shouldFixSafariAutoComplete ? `${country.countryCode} ${value}` : value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleKeepSessionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const handleKeepSessionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setAuthRememberMe({ value: e.target.checked });
|
setAuthRememberMe({ value: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -235,7 +234,11 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
goToAuthQrCode();
|
goToAuthQrCode();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAuthReady = authState === 'authorizationStateWaitPhoneNumber';
|
const handleLoginWithPasskey = useLastCallback(() => {
|
||||||
|
loginWithPasskey();
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAuthReady = state === 'authorizationStateWaitPhoneNumber';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="auth-phone-number-form" className="custom-scroll">
|
<div id="auth-phone-number-form" className="custom-scroll">
|
||||||
@ -257,7 +260,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
<CountryCodeInput
|
<CountryCodeInput
|
||||||
id="sign-in-phone-code"
|
id="sign-in-phone-code"
|
||||||
value={country}
|
value={country}
|
||||||
isLoading={!authNearestCountry && !country}
|
isLoading={!nearestCountry && !country}
|
||||||
onChange={handleCountryChange}
|
onChange={handleCountryChange}
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
@ -265,7 +268,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
id="sign-in-phone-number"
|
id="sign-in-phone-number"
|
||||||
label={lang('LoginPhonePlaceholder')}
|
label={lang('LoginPhonePlaceholder')}
|
||||||
value={fullNumber}
|
value={fullNumber}
|
||||||
error={authErrorKey && lang.withRegular(authErrorKey)}
|
error={errorKey && lang.withRegular(errorKey)}
|
||||||
inputMode="tel"
|
inputMode="tel"
|
||||||
onChange={handlePhoneNumberChange}
|
onChange={handlePhoneNumberChange}
|
||||||
onPaste={IS_SAFARI ? handlePaste : undefined}
|
onPaste={IS_SAFARI ? handlePaste : undefined}
|
||||||
@ -273,7 +276,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="sign-in-keep-session"
|
id="sign-in-keep-session"
|
||||||
label={lang('AuthKeepSignedIn')}
|
label={lang('AuthKeepSignedIn')}
|
||||||
checked={Boolean(authRememberMe)}
|
checked={Boolean(rememberMe)}
|
||||||
onChange={handleKeepSessionChange}
|
onChange={handleKeepSessionChange}
|
||||||
/>
|
/>
|
||||||
{canSubmit && (
|
{canSubmit && (
|
||||||
@ -295,12 +298,17 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
className="auth-button"
|
className="auth-button"
|
||||||
isText
|
isText
|
||||||
ripple
|
ripple
|
||||||
isLoading={authIsLoadingQrCode}
|
isLoading={isLoadingQrCode}
|
||||||
onClick={handleGoToAuthQrCode}
|
onClick={handleGoToAuthQrCode}
|
||||||
>
|
>
|
||||||
{lang('LoginQRLogin')}
|
{lang('LoginQRLogin')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{passkeyOption && (
|
||||||
|
<Button className="auth-button" isText onClick={handleLoginWithPasskey}>
|
||||||
|
{lang('LoginPasskey')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
||||||
<Button
|
<Button
|
||||||
className="auth-button"
|
className="auth-button"
|
||||||
@ -323,22 +331,16 @@ export default memo(withGlobal(
|
|||||||
sharedState: { settings: { language } },
|
sharedState: { settings: { language } },
|
||||||
countryList: { phoneCodes: phoneCodeList },
|
countryList: { phoneCodes: phoneCodeList },
|
||||||
config,
|
config,
|
||||||
|
auth,
|
||||||
|
connectionState,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...pick(global, [
|
auth,
|
||||||
'connectionState',
|
connectionState,
|
||||||
'authState',
|
|
||||||
'authPhoneNumber',
|
|
||||||
'authIsLoading',
|
|
||||||
'authIsLoadingQrCode',
|
|
||||||
'authErrorKey',
|
|
||||||
'authRememberMe',
|
|
||||||
'authNearestCountry',
|
|
||||||
]),
|
|
||||||
language,
|
language,
|
||||||
phoneCodeList,
|
phoneCodeList,
|
||||||
isTestServer: config?.isTestServer,
|
isTestServer: config?.isTestServer,
|
||||||
} as Complete<StateProps>;
|
};
|
||||||
},
|
},
|
||||||
)(AuthPhoneNumber));
|
)(AuthPhoneNumber));
|
||||||
|
|||||||
@ -28,11 +28,11 @@ import Loading from '../ui/Loading';
|
|||||||
|
|
||||||
import blankUrl from '../../assets/blank.png';
|
import blankUrl from '../../assets/blank.png';
|
||||||
|
|
||||||
type StateProps =
|
type StateProps = {
|
||||||
Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'>
|
auth: GlobalState['auth'];
|
||||||
& {
|
connectionState: GlobalState['connectionState'];
|
||||||
language?: string;
|
language?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATA_PREFIX = 'tg://login?token=';
|
const DATA_PREFIX = 'tg://login?token=';
|
||||||
const QR_SIZE = 280;
|
const QR_SIZE = 280;
|
||||||
@ -50,15 +50,17 @@ function ensureQrCodeStyling() {
|
|||||||
|
|
||||||
const AuthCode = ({
|
const AuthCode = ({
|
||||||
connectionState,
|
connectionState,
|
||||||
authState,
|
auth,
|
||||||
authQrCode,
|
|
||||||
language,
|
language,
|
||||||
}: StateProps) => {
|
}: StateProps) => {
|
||||||
const {
|
const {
|
||||||
returnToAuthPhoneNumber,
|
returnToAuthPhoneNumber,
|
||||||
setSharedSettingOption,
|
setSharedSettingOption,
|
||||||
|
loginWithPasskey,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const { state, qrCode: authQrCode, passkeyOption } = auth;
|
||||||
|
|
||||||
const suggestedLanguage = getSuggestedLanguage();
|
const suggestedLanguage = getSuggestedLanguage();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const qrCodeRef = useRef<HTMLDivElement>();
|
const qrCodeRef = useRef<HTMLDivElement>();
|
||||||
@ -151,7 +153,11 @@ const AuthCode = ({
|
|||||||
returnToAuthPhoneNumber();
|
returnToAuthPhoneNumber();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAuthReady = authState === 'authorizationStateWaitQrCode';
|
const handleLoginWithPasskey = useLastCallback(() => {
|
||||||
|
loginWithPasskey();
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAuthReady = state === 'authorizationStateWaitQrCode';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="auth-qr-form" className="custom-scroll">
|
<div id="auth-qr-form" className="custom-scroll">
|
||||||
@ -198,6 +204,11 @@ const AuthCode = ({
|
|||||||
{lang('LoginQRCancel')}
|
{lang('LoginQRCancel')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{passkeyOption && (
|
||||||
|
<Button className="auth-button" isText onClick={handleLoginWithPasskey}>
|
||||||
|
{lang('LoginPasskey')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
||||||
<Button className="auth-button" isText isLoading={isLoading} onClick={handleLangChange}>
|
<Button className="auth-button" isText isLoading={isLoading} onClick={handleLangChange}>
|
||||||
{continueText}
|
{continueText}
|
||||||
@ -211,15 +222,14 @@ const AuthCode = ({
|
|||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => {
|
(global): Complete<StateProps> => {
|
||||||
const {
|
const {
|
||||||
connectionState, authState, authQrCode,
|
connectionState, auth,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
const { language } = selectSharedSettings(global);
|
const { language } = selectSharedSettings(global);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionState,
|
connectionState,
|
||||||
authState,
|
auth,
|
||||||
authQrCode,
|
|
||||||
language,
|
language,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import type React from '../../lib/teact/teact';
|
|
||||||
import { memo, useCallback, useState } from '../../lib/teact/teact';
|
import { memo, useCallback, useState } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -14,12 +11,15 @@ import AvatarEditable from '../ui/AvatarEditable';
|
|||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import InputText from '../ui/InputText';
|
import InputText from '../ui/InputText';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authIsLoading' | 'authErrorKey'>;
|
type StateProps = {
|
||||||
|
auth: GlobalState['auth'];
|
||||||
|
};
|
||||||
|
|
||||||
const AuthRegister: FC<StateProps> = ({
|
const AuthRegister = ({
|
||||||
authIsLoading, authErrorKey,
|
auth,
|
||||||
}) => {
|
}: StateProps) => {
|
||||||
const { signUp, clearAuthErrorKey, uploadProfilePhoto } = getActions();
|
const { signUp, clearAuthErrorKey, uploadProfilePhoto } = getActions();
|
||||||
|
const { isLoading, errorKey } = auth;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [isButtonShown, setIsButtonShown] = useState(false);
|
const [isButtonShown, setIsButtonShown] = useState(false);
|
||||||
@ -27,8 +27,8 @@ const AuthRegister: FC<StateProps> = ({
|
|||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
const [lastName, setLastName] = useState('');
|
const [lastName, setLastName] = useState('');
|
||||||
|
|
||||||
const handleFirstNameChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
const handleFirstNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (authErrorKey) {
|
if (errorKey) {
|
||||||
clearAuthErrorKey();
|
clearAuthErrorKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +36,9 @@ const AuthRegister: FC<StateProps> = ({
|
|||||||
|
|
||||||
setFirstName(target.value);
|
setFirstName(target.value);
|
||||||
setIsButtonShown(target.value.length > 0);
|
setIsButtonShown(target.value.length > 0);
|
||||||
}, [authErrorKey]);
|
}, [errorKey]);
|
||||||
|
|
||||||
const handleLastNameChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
const handleLastNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { target } = event;
|
const { target } = event;
|
||||||
|
|
||||||
setLastName(target.value);
|
setLastName(target.value);
|
||||||
@ -66,7 +66,7 @@ const AuthRegister: FC<StateProps> = ({
|
|||||||
label={lang('LoginRegisterFirstNamePlaceholder')}
|
label={lang('LoginRegisterFirstNamePlaceholder')}
|
||||||
onChange={handleFirstNameChange}
|
onChange={handleFirstNameChange}
|
||||||
value={firstName}
|
value={firstName}
|
||||||
error={authErrorKey && lang.withRegular(authErrorKey)}
|
error={errorKey && lang.withRegular(errorKey)}
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
@ -77,7 +77,7 @@ const AuthRegister: FC<StateProps> = ({
|
|||||||
autoComplete="family-name"
|
autoComplete="family-name"
|
||||||
/>
|
/>
|
||||||
{isButtonShown && (
|
{isButtonShown && (
|
||||||
<Button type="submit" ripple isLoading={authIsLoading}>{lang('Next')}</Button>
|
<Button type="submit" ripple isLoading={isLoading}>{lang('Next')}</Button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -87,6 +87,6 @@ const AuthRegister: FC<StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => (
|
(global): Complete<StateProps> => (
|
||||||
pick(global, ['authIsLoading', 'authErrorKey']) as Complete<StateProps>
|
pick(global, ['auth'])
|
||||||
),
|
),
|
||||||
)(AuthRegister));
|
)(AuthRegister));
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -13,7 +12,7 @@ type StateProps = {
|
|||||||
notifications: ApiNotification[];
|
notifications: ApiNotification[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const Notifications: FC<StateProps> = ({ notifications }) => {
|
const Notifications = ({ notifications }: StateProps) => {
|
||||||
if (!notifications.length) {
|
if (!notifications.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -33,15 +33,18 @@ import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
|||||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||||
import FoldersShare from '../../../assets/tgs/settings/FoldersShare.tgs';
|
import FoldersShare from '../../../assets/tgs/settings/FoldersShare.tgs';
|
||||||
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||||
|
import Passkeys from '../../../assets/tgs/settings/Passkeys.tgs';
|
||||||
import StarReaction from '../../../assets/tgs/stars/StarReaction.tgs';
|
import StarReaction from '../../../assets/tgs/stars/StarReaction.tgs';
|
||||||
import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs';
|
import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs';
|
||||||
import Unlock from '../../../assets/tgs/Unlock.tgs';
|
import Unlock from '../../../assets/tgs/Unlock.tgs';
|
||||||
import DuckNothingFoundPreview from '../../../assets/tgs-previews/DuckNothingFound.svg';
|
import DuckNothingFoundPreview from '../../../assets/tgs-previews/DuckNothingFound.svg';
|
||||||
import SearchPreview from '../../../assets/tgs-previews/Search.svg';
|
import SearchPreview from '../../../assets/tgs-previews/Search.svg';
|
||||||
|
import PasskeysPreview from '../../../assets/tgs-previews/settings/Passkeys.svg';
|
||||||
|
|
||||||
export const LOCAL_TGS_PREVIEW_URLS = {
|
export const LOCAL_TGS_PREVIEW_URLS = {
|
||||||
DuckNothingFound: DuckNothingFoundPreview,
|
DuckNothingFound: DuckNothingFoundPreview,
|
||||||
Search: SearchPreview,
|
Search: SearchPreview,
|
||||||
|
Passkeys: PasskeysPreview,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOCAL_TGS_URLS = {
|
export const LOCAL_TGS_URLS = {
|
||||||
@ -82,5 +85,6 @@ export const LOCAL_TGS_URLS = {
|
|||||||
Diamond,
|
Diamond,
|
||||||
Search,
|
Search,
|
||||||
DuckNothingFound,
|
DuckNothingFound,
|
||||||
|
Passkeys,
|
||||||
DuckCake,
|
DuckCake,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -218,6 +218,7 @@ function LeftColumn({
|
|||||||
case SettingsScreens.PasscodeDisabled:
|
case SettingsScreens.PasscodeDisabled:
|
||||||
case SettingsScreens.PasscodeEnabled:
|
case SettingsScreens.PasscodeEnabled:
|
||||||
case SettingsScreens.PasscodeCongratulations:
|
case SettingsScreens.PasscodeCongratulations:
|
||||||
|
case SettingsScreens.Passkeys:
|
||||||
openSettingsScreen({ screen: SettingsScreens.Privacy });
|
openSettingsScreen({ screen: SettingsScreens.Privacy });
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import SettingsHeader from './SettingsHeader';
|
|||||||
import SettingsLanguage from './SettingsLanguage';
|
import SettingsLanguage from './SettingsLanguage';
|
||||||
import SettingsMain from './SettingsMain';
|
import SettingsMain from './SettingsMain';
|
||||||
import SettingsNotifications from './SettingsNotifications';
|
import SettingsNotifications from './SettingsNotifications';
|
||||||
|
import SettingsPasskeys from './SettingsPasskeys';
|
||||||
import SettingsPerformance from './SettingsPerformance';
|
import SettingsPerformance from './SettingsPerformance';
|
||||||
import SettingsPrivacy from './SettingsPrivacy';
|
import SettingsPrivacy from './SettingsPrivacy';
|
||||||
import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
import SettingsPrivacyBlockedUsers from './SettingsPrivacyBlockedUsers';
|
||||||
@ -85,6 +86,7 @@ const FOLDERS_SCREENS = [
|
|||||||
const PRIVACY_SCREENS = [
|
const PRIVACY_SCREENS = [
|
||||||
SettingsScreens.PrivacyBlockedUsers,
|
SettingsScreens.PrivacyBlockedUsers,
|
||||||
SettingsScreens.ActiveWebsites,
|
SettingsScreens.ActiveWebsites,
|
||||||
|
SettingsScreens.Passkeys,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRIVACY_PHONE_NUMBER_SCREENS = [
|
const PRIVACY_PHONE_NUMBER_SCREENS = [
|
||||||
@ -486,6 +488,14 @@ const Settings: FC<OwnProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.Passkeys:
|
||||||
|
return (
|
||||||
|
<SettingsPasskeys
|
||||||
|
isActive={isScreenActive}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -253,6 +253,10 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.Passkeys:
|
||||||
|
return <h3>{lang('SettingsPasskeyTitle')}</h3>;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="settings-main-header">
|
<div className="settings-main-header">
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
.icon {
|
||||||
|
--custom-emoji-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallbackIcon {
|
||||||
|
padding-inline: 0.25rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
180
src/components/left/settings/SettingsPasskeys.tsx
Normal file
180
src/components/left/settings/SettingsPasskeys.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiPasskey } from '../../../api/types';
|
||||||
|
|
||||||
|
import { IS_WEBAUTHN_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatPastDatetime } from '../../../util/dates/dateFormat';
|
||||||
|
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||||
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
import Link from '../../ui/Link';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
|
||||||
|
import styles from './SettingsPasskeys.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
passkeys?: ApiPasskey[];
|
||||||
|
maxPasskeysCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOP_STICKER_SIZE = 120;
|
||||||
|
const ICON_SIZE = 2 * REM;
|
||||||
|
|
||||||
|
const SettingsPasskeys = ({
|
||||||
|
isActive,
|
||||||
|
passkeys,
|
||||||
|
maxPasskeysCount,
|
||||||
|
onReset,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
startPasskeyRegistration,
|
||||||
|
deletePasskey,
|
||||||
|
openPasskeyModal,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [deleteModalId, setDeleteModalId] = useState<string>();
|
||||||
|
|
||||||
|
const canAddPasskey = IS_WEBAUTHN_SUPPORTED && (passkeys?.length ?? 0) < maxPasskeysCount;
|
||||||
|
|
||||||
|
const handleCreatePasskey = useLastCallback(() => {
|
||||||
|
startPasskeyRegistration();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenPasskeyModal = useLastCallback(() => {
|
||||||
|
openPasskeyModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmDeletePasskey = useLastCallback(() => {
|
||||||
|
if (!deleteModalId) return;
|
||||||
|
deletePasskey({ id: deleteModalId });
|
||||||
|
setDeleteModalId(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!passkeys || passkeys.length || !isActive) return;
|
||||||
|
onReset(); // Autoclose when last passkey is deleted
|
||||||
|
}, [passkeys, onReset, isActive]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderPasskey(passkey: ApiPasskey) {
|
||||||
|
const { softwareEmojiId, id, name, date, lastUsageDate } = passkey;
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={id}
|
||||||
|
ripple
|
||||||
|
narrow
|
||||||
|
contextActions={[{
|
||||||
|
title: lang('Delete'),
|
||||||
|
icon: 'delete',
|
||||||
|
destructive: true,
|
||||||
|
handler: () => {
|
||||||
|
setDeleteModalId(id);
|
||||||
|
},
|
||||||
|
}]}
|
||||||
|
leftElement={softwareEmojiId ? (
|
||||||
|
<CustomEmoji
|
||||||
|
size={ICON_SIZE}
|
||||||
|
className={buildClassName(styles.icon, 'ListItem-main-icon')}
|
||||||
|
documentId={softwareEmojiId}
|
||||||
|
noPlay
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon name="lock" className={buildClassName(styles.fallbackIcon, 'ListItem-main-icon')} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="multiline-item full-size" dir="auto">
|
||||||
|
<span className="date">{formatPastDatetime(lang, date)}</span>
|
||||||
|
<span className="title">{name || lang('SettingsPasskeyFallbackTitle')}</span>
|
||||||
|
{Boolean(lastUsageDate) && (
|
||||||
|
<span className="subtitle">
|
||||||
|
{lang('SettingsPasskeyUsedAt', {
|
||||||
|
date: formatPastDatetime(lang, lastUsageDate),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content custom-scroll">
|
||||||
|
<div className="settings-content-header">
|
||||||
|
<AnimatedIconWithPreview
|
||||||
|
tgsUrl={LOCAL_TGS_URLS.Passkeys}
|
||||||
|
previewUrl={LOCAL_TGS_PREVIEW_URLS.Passkeys}
|
||||||
|
size={TOP_STICKER_SIZE}
|
||||||
|
className="settings-content-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="settings-item-description" dir="auto">
|
||||||
|
{lang('SettingsPasskeyInfo')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="settings-item">
|
||||||
|
{passkeys?.map(renderPasskey)}
|
||||||
|
{canAddPasskey && (
|
||||||
|
<Button
|
||||||
|
className="settings-button"
|
||||||
|
color="primary"
|
||||||
|
iconName="add"
|
||||||
|
isText
|
||||||
|
noForcedUpperCase
|
||||||
|
onClick={handleCreatePasskey}
|
||||||
|
>
|
||||||
|
{lang('SettingsPasskeysCreate')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<p className="settings-item-description mt-3" dir="auto">
|
||||||
|
{lang('SettingsPasskeysFooter', {
|
||||||
|
link: <Link isPrimary onClick={handleOpenPasskeyModal}>{lang('SettingsPasskeysFooterLink')}</Link>,
|
||||||
|
}, { withNodes: true })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={Boolean(deleteModalId)}
|
||||||
|
title={lang('PasskeyDeleteTitle')}
|
||||||
|
textParts={lang('PasskeyDeleteText', undefined, { withNodes: true, renderTextFilters: ['br'] })}
|
||||||
|
confirmHandler={confirmDeletePasskey}
|
||||||
|
confirmIsDestructive
|
||||||
|
confirmLabel={lang('Delete')}
|
||||||
|
onClose={() => setDeleteModalId(undefined)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
passkeys: global.settings.passkeys,
|
||||||
|
maxPasskeysCount: global.appConfig.passkeysMaxCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(SettingsPasskeys));
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import { memo, useEffect, useMemo } from '../../../lib/teact/teact';
|
||||||
import { memo, useCallback, useEffect, useMemo } from '../../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiPrivacySettings } from '../../../api/types';
|
import type { ApiPrivacySettings } from '../../../api/types';
|
||||||
@ -47,17 +46,21 @@ type StateProps = {
|
|||||||
needAgeVideoVerification?: boolean;
|
needAgeVideoVerification?: boolean;
|
||||||
privacy: GlobalState['settings']['privacy'];
|
privacy: GlobalState['settings']['privacy'];
|
||||||
accountDaysTtl?: number;
|
accountDaysTtl?: number;
|
||||||
|
passkeyCount?: number;
|
||||||
|
arePasskeysAvailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DAYS_PER_MONTH = 30;
|
const DAYS_PER_MONTH = 30;
|
||||||
|
|
||||||
const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
const SettingsPrivacy = ({
|
||||||
isActive,
|
isActive,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
hasPassword,
|
hasPassword,
|
||||||
hasPasscode,
|
hasPasscode,
|
||||||
blockedCount,
|
blockedCount,
|
||||||
webAuthCount,
|
webAuthCount,
|
||||||
|
passkeyCount,
|
||||||
|
arePasskeysAvailable,
|
||||||
isSensitiveEnabled,
|
isSensitiveEnabled,
|
||||||
canChangeSensitive,
|
canChangeSensitive,
|
||||||
canDisplayAutoarchiveSetting,
|
canDisplayAutoarchiveSetting,
|
||||||
@ -71,7 +74,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
isCurrentUserFrozen,
|
isCurrentUserFrozen,
|
||||||
accountDaysTtl,
|
accountDaysTtl,
|
||||||
onReset,
|
onReset,
|
||||||
}) => {
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
openDeleteAccountModal,
|
openDeleteAccountModal,
|
||||||
loadPrivacySettings,
|
loadPrivacySettings,
|
||||||
@ -84,6 +87,8 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
openSettingsScreen,
|
openSettingsScreen,
|
||||||
loadAccountDaysTtl,
|
loadAccountDaysTtl,
|
||||||
openAgeVerificationModal,
|
openAgeVerificationModal,
|
||||||
|
loadPasskeys,
|
||||||
|
openPasskeyModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -91,6 +96,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
loadBlockedUsers();
|
loadBlockedUsers();
|
||||||
loadPrivacySettings({});
|
loadPrivacySettings({});
|
||||||
loadWebAuthorizations();
|
loadWebAuthorizations();
|
||||||
|
loadPasskeys();
|
||||||
}
|
}
|
||||||
}, [isCurrentUserFrozen]);
|
}, [isCurrentUserFrozen]);
|
||||||
|
|
||||||
@ -99,7 +105,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
loadGlobalPrivacySettings();
|
loadGlobalPrivacySettings();
|
||||||
loadAccountDaysTtl();
|
loadAccountDaysTtl();
|
||||||
}
|
}
|
||||||
}, [isActive, isCurrentUserFrozen, loadGlobalPrivacySettings]);
|
}, [isActive, isCurrentUserFrozen]);
|
||||||
|
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -109,31 +115,41 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleArchiveAndMuteChange = useCallback((isEnabled: boolean) => {
|
const handleArchiveAndMuteChange = useLastCallback((isEnabled: boolean) => {
|
||||||
updateGlobalPrivacySettings({
|
updateGlobalPrivacySettings({
|
||||||
shouldArchiveAndMuteNewNonContact: isEnabled,
|
shouldArchiveAndMuteNewNonContact: isEnabled,
|
||||||
});
|
});
|
||||||
}, [updateGlobalPrivacySettings]);
|
});
|
||||||
|
|
||||||
const handleChatInTitleChange = useCallback((isChecked: boolean) => {
|
const handleChatInTitleChange = useLastCallback((isChecked: boolean) => {
|
||||||
setSharedSettingOption({
|
setSharedSettingOption({
|
||||||
canDisplayChatInTitle: isChecked,
|
canDisplayChatInTitle: isChecked,
|
||||||
});
|
});
|
||||||
}, []);
|
});
|
||||||
|
|
||||||
const handleUpdateContentSettings = useCallback((isChecked: boolean) => {
|
const handleUpdateContentSettings = useLastCallback((isChecked: boolean) => {
|
||||||
updateContentSettings({ isSensitiveEnabled: isChecked });
|
updateContentSettings({ isSensitiveEnabled: isChecked });
|
||||||
}, [updateContentSettings]);
|
});
|
||||||
|
|
||||||
const handleAgeVerification = useCallback(() => {
|
const handleAgeVerification = useLastCallback(() => {
|
||||||
openAgeVerificationModal();
|
openAgeVerificationModal();
|
||||||
}, [openAgeVerificationModal]);
|
});
|
||||||
|
|
||||||
const handleOpenDeleteAccountModal = useLastCallback(() => {
|
const handleOpenDeleteAccountModal = useLastCallback(() => {
|
||||||
if (!accountDaysTtl) return;
|
if (!accountDaysTtl) return;
|
||||||
openDeleteAccountModal({ days: accountDaysTtl });
|
openDeleteAccountModal({ days: accountDaysTtl });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenPasskeys = useLastCallback(() => {
|
||||||
|
if (!arePasskeysAvailable || passkeyCount === undefined) return;
|
||||||
|
if (passkeyCount === 0) {
|
||||||
|
openPasskeyModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openSettingsScreen({ screen: SettingsScreens.Passkeys });
|
||||||
|
});
|
||||||
|
|
||||||
const dayOption = useMemo(() => {
|
const dayOption = useMemo(() => {
|
||||||
if (!accountDaysTtl) return undefined;
|
if (!accountDaysTtl) return undefined;
|
||||||
return getClosestEntry(ACCOUNT_TTL_OPTIONS, accountDaysTtl / DAYS_PER_MONTH).toString();
|
return getClosestEntry(ACCOUNT_TTL_OPTIONS, accountDaysTtl / DAYS_PER_MONTH).toString();
|
||||||
@ -192,7 +208,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
{canSetPasscode && (
|
{canSetPasscode && (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="key"
|
icon="lock"
|
||||||
narrow
|
narrow
|
||||||
|
|
||||||
onClick={() => openSettingsScreen({
|
onClick={() => openSettingsScreen({
|
||||||
@ -202,13 +218,13 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="multiline-item">
|
<div className="multiline-item">
|
||||||
<span className="title">{oldLang('Passcode')}</span>
|
<span className="title">{oldLang('Passcode')}</span>
|
||||||
<span className="subtitle" dir="auto">
|
<span className="subtitle" dir="auto">
|
||||||
{oldLang(hasPasscode ? 'PasswordOn' : 'PasswordOff')}
|
{lang(hasPasscode ? 'SettingsItemPrivacyOn' : 'SettingsItemPrivacyOff')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="lock"
|
icon="admin"
|
||||||
narrow
|
narrow
|
||||||
|
|
||||||
onClick={() => openSettingsScreen({
|
onClick={() => openSettingsScreen({
|
||||||
@ -218,10 +234,25 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="multiline-item">
|
<div className="multiline-item">
|
||||||
<span className="title">{oldLang('TwoStepVerification')}</span>
|
<span className="title">{oldLang('TwoStepVerification')}</span>
|
||||||
<span className="subtitle" dir="auto">
|
<span className="subtitle" dir="auto">
|
||||||
{oldLang(hasPassword ? 'PasswordOn' : 'PasswordOff')}
|
{lang(hasPassword ? 'SettingsItemPrivacyOn' : 'SettingsItemPrivacyOff')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
{arePasskeysAvailable && (
|
||||||
|
<ListItem
|
||||||
|
icon="key"
|
||||||
|
narrow
|
||||||
|
onClick={handleOpenPasskeys}
|
||||||
|
>
|
||||||
|
<div className="multiline-item">
|
||||||
|
<span className="title">{lang('SettingsItemPrivacyPasskeys')}</span>
|
||||||
|
<span className="subtitle" dir="auto">
|
||||||
|
{lang(passkeyCount === undefined ? 'Loading'
|
||||||
|
: passkeyCount > 0 ? 'SettingsItemPrivacyOn' : 'SettingsItemPrivacyOff')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
{webAuthCount > 0 && (
|
{webAuthCount > 0 && (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="web"
|
icon="web"
|
||||||
@ -470,6 +501,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
},
|
},
|
||||||
privacy,
|
privacy,
|
||||||
accountDaysTtl,
|
accountDaysTtl,
|
||||||
|
passkeys,
|
||||||
},
|
},
|
||||||
blocked,
|
blocked,
|
||||||
passcode: {
|
passcode: {
|
||||||
@ -501,6 +533,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canSetPasscode: selectCanSetPasscode(global),
|
canSetPasscode: selectCanSetPasscode(global),
|
||||||
isCurrentUserFrozen,
|
isCurrentUserFrozen,
|
||||||
accountDaysTtl,
|
accountDaysTtl,
|
||||||
|
passkeyCount: passkeys?.length,
|
||||||
|
arePasskeysAvailable: appConfig.arePasskeysAvailable,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsPrivacy));
|
)(SettingsPrivacy));
|
||||||
|
|||||||
@ -85,7 +85,6 @@ import ForwardRecipientPicker from './ForwardRecipientPicker.async';
|
|||||||
import GameModal from './GameModal';
|
import GameModal from './GameModal';
|
||||||
import HistoryCalendar from './HistoryCalendar.async';
|
import HistoryCalendar from './HistoryCalendar.async';
|
||||||
import NewContactModal from './NewContactModal.async';
|
import NewContactModal from './NewContactModal.async';
|
||||||
import Notifications from './Notifications.async';
|
|
||||||
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
||||||
import GiveawayModal from './premium/GiveawayModal.async';
|
import GiveawayModal from './premium/GiveawayModal.async';
|
||||||
import PremiumMainModal from './premium/PremiumMainModal.async';
|
import PremiumMainModal from './premium/PremiumMainModal.async';
|
||||||
@ -110,7 +109,6 @@ type StateProps = {
|
|||||||
isMediaViewerOpen: boolean;
|
isMediaViewerOpen: boolean;
|
||||||
isStoryViewerOpen: boolean;
|
isStoryViewerOpen: boolean;
|
||||||
isForwardModalOpen: boolean;
|
isForwardModalOpen: boolean;
|
||||||
hasNotifications: boolean;
|
|
||||||
hasDialogs: boolean;
|
hasDialogs: boolean;
|
||||||
safeLinkModalUrl?: string;
|
safeLinkModalUrl?: string;
|
||||||
isHistoryCalendarOpen: boolean;
|
isHistoryCalendarOpen: boolean;
|
||||||
@ -163,7 +161,6 @@ const Main = ({
|
|||||||
isMediaViewerOpen,
|
isMediaViewerOpen,
|
||||||
isStoryViewerOpen,
|
isStoryViewerOpen,
|
||||||
isForwardModalOpen,
|
isForwardModalOpen,
|
||||||
hasNotifications,
|
|
||||||
hasDialogs,
|
hasDialogs,
|
||||||
activeGroupCallId,
|
activeGroupCallId,
|
||||||
safeLinkModalUrl,
|
safeLinkModalUrl,
|
||||||
@ -564,7 +561,6 @@ const Main = ({
|
|||||||
<StoryViewer isOpen={isStoryViewerOpen} />
|
<StoryViewer isOpen={isStoryViewerOpen} />
|
||||||
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
|
<ForwardRecipientPicker isOpen={isForwardModalOpen} />
|
||||||
<DraftRecipientPicker requestedDraft={requestedDraft} />
|
<DraftRecipientPicker requestedDraft={requestedDraft} />
|
||||||
<Notifications isOpen={hasNotifications} />
|
|
||||||
<Dialogs isOpen={hasDialogs} />
|
<Dialogs isOpen={hasDialogs} />
|
||||||
<AudioPlayer noUi />
|
<AudioPlayer noUi />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
@ -631,7 +627,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
openedGame,
|
openedGame,
|
||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
historyCalendarSelectedAt,
|
historyCalendarSelectedAt,
|
||||||
notifications,
|
|
||||||
dialogs,
|
dialogs,
|
||||||
newContact,
|
newContact,
|
||||||
ratingPhoneCall,
|
ratingPhoneCall,
|
||||||
@ -665,7 +660,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isStoryViewerOpen: selectIsStoryViewerOpen(global),
|
isStoryViewerOpen: selectIsStoryViewerOpen(global),
|
||||||
isForwardModalOpen: selectIsForwardModalOpen(global),
|
isForwardModalOpen: selectIsForwardModalOpen(global),
|
||||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||||
hasNotifications: Boolean(notifications.length),
|
|
||||||
hasDialogs: Boolean(dialogs.length),
|
hasDialogs: Boolean(dialogs.length),
|
||||||
safeLinkModalUrl,
|
safeLinkModalUrl,
|
||||||
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
|
|
||||||
import { Bundles } from '../../util/moduleLoader';
|
|
||||||
|
|
||||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
|
||||||
|
|
||||||
const NotificationsAsync: FC = ({ isOpen }) => {
|
|
||||||
const Notifications = useModuleLoader(Bundles.Extra, 'Notifications', !isOpen);
|
|
||||||
|
|
||||||
return Notifications ? <Notifications /> : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotificationsAsync;
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import { type FC, memo } from '../../lib/teact/teact';
|
||||||
import { memo } from '../../lib/teact/teact';
|
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { TabState } from '../../global/types';
|
import type { TabState } from '../../global/types';
|
||||||
@ -39,6 +38,7 @@ import LocationAccessModal from './locationAccess/LocationAccessModal.async';
|
|||||||
import MapModal from './map/MapModal.async';
|
import MapModal from './map/MapModal.async';
|
||||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||||
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
||||||
|
import PasskeyModal from './passkey/PasskeyModal.async';
|
||||||
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
||||||
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
||||||
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
||||||
@ -111,6 +111,7 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'profileRatingModal' |
|
'profileRatingModal' |
|
||||||
'quickPreview' |
|
'quickPreview' |
|
||||||
'storyStealthModal' |
|
'storyStealthModal' |
|
||||||
|
'isPasskeyModalOpen' |
|
||||||
'birthdaySetupModal'
|
'birthdaySetupModal'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -177,6 +178,7 @@ const MODALS: ModalRegistry = {
|
|||||||
profileRatingModal: ProfileRatingModal,
|
profileRatingModal: ProfileRatingModal,
|
||||||
quickPreview: QuickPreviewModal,
|
quickPreview: QuickPreviewModal,
|
||||||
storyStealthModal: StealthModeModal,
|
storyStealthModal: StealthModeModal,
|
||||||
|
isPasskeyModalOpen: PasskeyModal,
|
||||||
birthdaySetupModal: BirthdaySetupModal,
|
birthdaySetupModal: BirthdaySetupModal,
|
||||||
};
|
};
|
||||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||||
|
|||||||
14
src/components/modals/passkey/PasskeyModal.async.tsx
Normal file
14
src/components/modals/passkey/PasskeyModal.async.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { OwnProps } from './PasskeyModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const PasskeyModalAsync = (props: OwnProps) => {
|
||||||
|
const { modal } = props;
|
||||||
|
const PasskeyModal = useModuleLoader(Bundles.Extra, 'PasskeyModal', !modal);
|
||||||
|
|
||||||
|
return PasskeyModal ? <PasskeyModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasskeyModalAsync;
|
||||||
4
src/components/modals/passkey/PasskeyModal.module.scss
Normal file
4
src/components/modals/passkey/PasskeyModal.module.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.title {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
78
src/components/modals/passkey/PasskeyModal.tsx
Normal file
78
src/components/modals/passkey/PasskeyModal.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type { TabState } from '../../../global/types';
|
||||||
|
|
||||||
|
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
|
||||||
|
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
|
||||||
|
|
||||||
|
import styles from './PasskeyModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: TabState['isPasskeyModalOpen'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOP_STICKER_SIZE = 120;
|
||||||
|
|
||||||
|
const PasskeyModal = ({
|
||||||
|
modal,
|
||||||
|
}: OwnProps) => {
|
||||||
|
const { closePasskeyModal, startPasskeyRegistration } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closePasskeyModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreatePasskey = useLastCallback(() => {
|
||||||
|
startPasskeyRegistration();
|
||||||
|
handleClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalData = useMemo(() => {
|
||||||
|
const header = (
|
||||||
|
<div className="flex-column-centered">
|
||||||
|
<AnimatedIconWithPreview
|
||||||
|
tgsUrl={LOCAL_TGS_URLS.Passkeys}
|
||||||
|
previewUrl={LOCAL_TGS_PREVIEW_URLS.Passkeys}
|
||||||
|
size={TOP_STICKER_SIZE}
|
||||||
|
/>
|
||||||
|
<h3 className={styles.title}>{lang('PasskeyModalTitle')}</h3>
|
||||||
|
<p>{lang('PasskeyModalDescription')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const listItemData: TableAboutData = [
|
||||||
|
['key', lang('PasskeyModalFeature1Title'),
|
||||||
|
lang('PasskeyModalFeature1Description')],
|
||||||
|
['animals', lang('PasskeyModalFeature2Title'),
|
||||||
|
lang('PasskeyModalFeature2Description')],
|
||||||
|
['lock', lang('PasskeyModalFeature3Title'),
|
||||||
|
lang('PasskeyModalFeature3Description')],
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
listItemData,
|
||||||
|
};
|
||||||
|
}, [lang]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableAboutModal
|
||||||
|
isOpen={Boolean(modal)}
|
||||||
|
listItemData={modalData.listItemData}
|
||||||
|
header={modalData.header}
|
||||||
|
buttonText={lang('PasskeyModalButtonText')}
|
||||||
|
onButtonClick={handleCreatePasskey}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(PasskeyModal);
|
||||||
@ -7,7 +7,8 @@ import type { GlobalState } from '../../global/types';
|
|||||||
import ErrorTest from './ErrorTest';
|
import ErrorTest from './ErrorTest';
|
||||||
import SubTest from './SubTest';
|
import SubTest from './SubTest';
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'authState'> & {
|
type StateProps = {
|
||||||
|
authState: GlobalState['auth']['state'];
|
||||||
globalRand: number;
|
globalRand: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ const Test: FC<StateProps> = ({ authState, globalRand }) => {
|
|||||||
export default withGlobal(
|
export default withGlobal(
|
||||||
(global): Complete<StateProps> => {
|
(global): Complete<StateProps> => {
|
||||||
return {
|
return {
|
||||||
authState: global.authState,
|
authState: global.auth.state,
|
||||||
globalRand: Math.random(),
|
globalRand: Math.random(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,16 +14,16 @@ document.ondblclick = () => {
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
};
|
};
|
||||||
|
|
||||||
type AStateProps = Pick<GlobalState, 'authState'> & {
|
type AStateProps = Pick<GlobalState, 'isSyncing'> & {
|
||||||
aValue: number;
|
aValue: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BStateProps = Pick<GlobalState, 'authState'> & {
|
type BStateProps = Pick<GlobalState, 'isSyncing'> & {
|
||||||
bValue: number;
|
bValue: number;
|
||||||
derivedAValue: number;
|
derivedAValue: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BOwnProps = Pick<GlobalState, 'authState'> & {
|
type BOwnProps = Pick<GlobalState, 'isSyncing'> & {
|
||||||
aValue: number;
|
aValue: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -19,12 +19,13 @@
|
|||||||
|
|
||||||
margin: 0 0.5rem;
|
margin: 0 0.5rem;
|
||||||
padding: 0.9375rem;
|
padding: 0.9375rem;
|
||||||
border-radius: var(--border-radius-default);
|
border-radius: var(--border-radius-toast);
|
||||||
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
background-color: rgba(32, 32, 32, 0.8);
|
background-color: var(--color-toast-background);
|
||||||
background-size: 1.5rem;
|
background-size: 1.5rem;
|
||||||
|
backdrop-filter: blur(0.5rem);
|
||||||
|
|
||||||
.text-entity-link,
|
.text-entity-link,
|
||||||
.text-entity-link:hover,
|
.text-entity-link:hover,
|
||||||
|
|||||||
@ -531,7 +531,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
|
|||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|
||||||
if (global.connectionState !== 'connectionStateReady' || global.authState !== 'authorizationStateReady') {
|
if (global.connectionState !== 'connectionStateReady' || global.auth.state !== 'authorizationStateReady') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,9 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { updateAppBadge } from '../../../util/appBadge';
|
import { updateAppBadge } from '../../../util/appBadge';
|
||||||
import { PASSCODE_IDB_STORE } from '../../../util/browser/idb';
|
import { PASSCODE_IDB_STORE } from '../../../util/browser/idb';
|
||||||
|
import { toCredentialRequestOptions } from '../../../util/browser/passkeys';
|
||||||
import {
|
import {
|
||||||
|
IS_WEBAUTHN_SUPPORTED,
|
||||||
IS_WEBM_SUPPORTED, MAX_BUFFER_SIZE, PLATFORM_ENV,
|
IS_WEBM_SUPPORTED, MAX_BUFFER_SIZE, PLATFORM_ENV,
|
||||||
} from '../../../util/browser/windowEnvironment';
|
} from '../../../util/browser/windowEnvironment';
|
||||||
import * as cacheApi from '../../../util/cacheApi';
|
import * as cacheApi from '../../../util/cacheApi';
|
||||||
@ -37,6 +39,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
clearGlobalForLockScreen, updateManagementProgress, updatePasscodeSettings,
|
clearGlobalForLockScreen, updateManagementProgress, updatePasscodeSettings,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
|
import { updateAuth } from '../../reducers/auth';
|
||||||
import { selectSharedSettings } from '../../selectors/sharedState';
|
import { selectSharedSettings } from '../../selectors/sharedState';
|
||||||
import { destroySharedStatePort } from '../../shared/sharedStateConnector';
|
import { destroySharedStatePort } from '../../shared/sharedStateConnector';
|
||||||
|
|
||||||
@ -74,6 +77,7 @@ addActionHandler('initApi', (global, actions): ActionReturnType => {
|
|||||||
langCode: language,
|
langCode: language,
|
||||||
isTestServerRequested: hasTestParam,
|
isTestServerRequested: hasTestParam,
|
||||||
accountIds,
|
accountIds,
|
||||||
|
hasPasskeySupport: IS_WEBAUTHN_SUPPORTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
void setShouldEnableDebugLog(Boolean(shouldCollectDebugLogs));
|
void setShouldEnableDebugLog(Boolean(shouldCollectDebugLogs));
|
||||||
@ -84,11 +88,10 @@ addActionHandler('setAuthPhoneNumber', (global, actions, payload): ActionReturnT
|
|||||||
|
|
||||||
void callApi('provideAuthPhoneNumber', phoneNumber.replace(/[^\d]/g, ''));
|
void callApi('provideAuthPhoneNumber', phoneNumber.replace(/[^\d]/g, ''));
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
isLoading: true,
|
||||||
authIsLoading: true,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('setAuthCode', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setAuthCode', (global, actions, payload): ActionReturnType => {
|
||||||
@ -96,11 +99,10 @@ addActionHandler('setAuthCode', (global, actions, payload): ActionReturnType =>
|
|||||||
|
|
||||||
void callApi('provideAuthCode', code);
|
void callApi('provideAuthCode', code);
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
isLoading: true,
|
||||||
authIsLoading: true,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('setAuthPassword', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setAuthPassword', (global, actions, payload): ActionReturnType => {
|
||||||
@ -108,11 +110,28 @@ addActionHandler('setAuthPassword', (global, actions, payload): ActionReturnType
|
|||||||
|
|
||||||
void callApi('provideAuthPassword', password);
|
void callApi('provideAuthPassword', password);
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
isLoading: true,
|
||||||
authIsLoading: true,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
|
addActionHandler('loginWithPasskey', async (global, actions, payload): Promise<void> => {
|
||||||
|
const passkeyOption = global.auth.passkeyOption;
|
||||||
|
if (!passkeyOption) return;
|
||||||
|
|
||||||
|
const credential = await navigator.credentials.get(toCredentialRequestOptions(passkeyOption)).catch((e: unknown) => {
|
||||||
|
actions.showNotification({
|
||||||
|
message: {
|
||||||
|
key: 'PasskeyLoginError',
|
||||||
|
},
|
||||||
|
tabId: getCurrentTabId(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!credential) return;
|
||||||
|
|
||||||
|
const publicKeyCredential = credential as PublicKeyCredential;
|
||||||
|
callApi('restartAuthWithPasskey', publicKeyCredential.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('uploadProfilePhoto', async (global, actions, payload): Promise<void> => {
|
addActionHandler('uploadProfilePhoto', async (global, actions, payload): Promise<void> => {
|
||||||
@ -139,30 +158,27 @@ addActionHandler('signUp', (global, actions, payload): ActionReturnType => {
|
|||||||
|
|
||||||
void callApi('provideAuthRegistration', { firstName, lastName });
|
void callApi('provideAuthRegistration', { firstName, lastName });
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
isLoading: true,
|
||||||
authIsLoading: true,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('returnToAuthPhoneNumber', (global): ActionReturnType => {
|
addActionHandler('returnToAuthPhoneNumber', (global): ActionReturnType => {
|
||||||
void callApi('restartAuth');
|
void callApi('restartAuth');
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('goToAuthQrCode', (global): ActionReturnType => {
|
addActionHandler('goToAuthQrCode', (global): ActionReturnType => {
|
||||||
void callApi('restartAuthWithQr');
|
void callApi('restartAuthWithQr');
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
isLoadingQrCode: true,
|
||||||
authIsLoadingQrCode: true,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('saveSession', (global, actions, payload): ActionReturnType => {
|
addActionHandler('saveSession', (global, actions, payload): ActionReturnType => {
|
||||||
@ -254,10 +270,9 @@ addActionHandler('loadNearestCountry', async (global): Promise<void> => {
|
|||||||
const authNearestCountry = await callApi('fetchNearestCountry');
|
const authNearestCountry = await callApi('fetchNearestCountry');
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
nearestCountry: authNearestCountry,
|
||||||
authNearestCountry,
|
});
|
||||||
};
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { ApiPrivacySettings, ApiUsername } from '../../../api/types';
|
|||||||
import type { ActionReturnType } from '../../types';
|
import type { ActionReturnType } from '../../types';
|
||||||
import {
|
import {
|
||||||
ProfileEditProgress,
|
ProfileEditProgress,
|
||||||
|
SettingsScreens,
|
||||||
UPLOADING_WALLPAPER_SLUG,
|
UPLOADING_WALLPAPER_SLUG,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
MUTE_INDEFINITE_TIMESTAMP,
|
MUTE_INDEFINITE_TIMESTAMP,
|
||||||
UNMUTE_TIMESTAMP,
|
UNMUTE_TIMESTAMP,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
|
import { toCredentialCreationOptions } from '../../../util/browser/passkeys';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications';
|
import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications';
|
||||||
@ -19,7 +21,7 @@ import requestActionTimeout from '../../../util/requestActionTimeout';
|
|||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { buildApiInputPrivacyRules } from '../../helpers';
|
import { buildApiInputPrivacyRules } from '../../helpers';
|
||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, getPromiseActions, setGlobal } from '../../index';
|
||||||
import {
|
import {
|
||||||
addBlockedUser, addNotifyExceptions, deletePeerPhoto,
|
addBlockedUser, addNotifyExceptions, deletePeerPhoto,
|
||||||
removeBlockedUser, replaceSettings, updateChat,
|
removeBlockedUser, replaceSettings, updateChat,
|
||||||
@ -636,9 +638,9 @@ addActionHandler('loadCountryList', async (global, actions, payload): Promise<vo
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('ensureTimeFormat', async (global, actions): Promise<void> => {
|
addActionHandler('ensureTimeFormat', async (global, actions): Promise<void> => {
|
||||||
if (global.authNearestCountry) {
|
if (global.auth.nearestCountry) {
|
||||||
const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT
|
const timeFormat = COUNTRIES_WITH_12H_TIME_FORMAT
|
||||||
.has(global.authNearestCountry.toUpperCase()) ? '12h' : '24h';
|
.has(global.auth.nearestCountry.toUpperCase()) ? '12h' : '24h';
|
||||||
actions.setSharedSettingOption({ timeFormat });
|
actions.setSharedSettingOption({ timeFormat });
|
||||||
setTimeFormat(timeFormat);
|
setTimeFormat(timeFormat);
|
||||||
}
|
}
|
||||||
@ -947,3 +949,79 @@ addActionHandler('sortChatUsernames', async (global, actions, payload): Promise<
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadPasskeys', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('fetchPasskeys');
|
||||||
|
if (!result) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
settings: {
|
||||||
|
...global.settings,
|
||||||
|
passkeys: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
settings: {
|
||||||
|
...global.settings,
|
||||||
|
passkeys: result.passkeys,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('startPasskeyRegistration', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
const preparedOptions = await callApi('initPasskeyRegistration');
|
||||||
|
if (!preparedOptions) return;
|
||||||
|
|
||||||
|
const options = toCredentialCreationOptions(preparedOptions);
|
||||||
|
const credential = await navigator.credentials.create(options).catch((e: unknown) => {
|
||||||
|
if (e instanceof DOMException && e.name === 'NotAllowedError') {
|
||||||
|
actions.showNotification({
|
||||||
|
message: {
|
||||||
|
key: 'PasskeyCreateError',
|
||||||
|
},
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
if (!credential) return;
|
||||||
|
const publicKeyCredential = credential as PublicKeyCredential;
|
||||||
|
|
||||||
|
const result = await callApi('registerPasskey', publicKeyCredential.toJSON());
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
await getPromiseActions().loadPasskeys();
|
||||||
|
actions.openSettingsScreen({ screen: SettingsScreens.Passkeys, tabId });
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('deletePasskey', async (global, actions, payload): Promise<void> => {
|
||||||
|
const { id } = payload;
|
||||||
|
|
||||||
|
const passkeys = global.settings.passkeys;
|
||||||
|
if (passkeys?.length) {
|
||||||
|
const filteredPasskeys = passkeys.filter((passkey) => passkey.id !== id);
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
settings: {
|
||||||
|
...global.settings,
|
||||||
|
passkeys: filteredPasskeys,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
await callApi('deletePasskey', { id });
|
||||||
|
|
||||||
|
actions.loadPasskeys();
|
||||||
|
});
|
||||||
|
|||||||
@ -313,15 +313,15 @@ function loadTopMessages<T extends GlobalState>(global: T, chatId: string, threa
|
|||||||
let previousGlobal: GlobalState | undefined;
|
let previousGlobal: GlobalState | undefined;
|
||||||
// RAF can be unreliable when device goes into sleep mode, so sync logic is handled outside any component
|
// RAF can be unreliable when device goes into sleep mode, so sync logic is handled outside any component
|
||||||
addCallback((global: GlobalState) => {
|
addCallback((global: GlobalState) => {
|
||||||
const { connectionState, authState, isSynced } = global;
|
const { connectionState, auth, isSynced } = global;
|
||||||
const { isMasterTab } = selectTabState(global);
|
const { isMasterTab } = selectTabState(global);
|
||||||
if (!isMasterTab || isSynced || (previousGlobal?.connectionState === connectionState
|
if (!isMasterTab || isSynced || (previousGlobal?.connectionState === connectionState
|
||||||
&& previousGlobal?.authState === authState)) {
|
&& previousGlobal?.auth.state === auth.state)) {
|
||||||
previousGlobal = global;
|
previousGlobal = global;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connectionState === 'connectionStateReady' && authState === 'authorizationStateReady') {
|
if (connectionState === 'connectionStateReady' && auth.state === 'authorizationStateReady') {
|
||||||
getActions().sync();
|
getActions().sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,10 @@ import type {
|
|||||||
ApiUpdateAuthorizationState,
|
ApiUpdateAuthorizationState,
|
||||||
ApiUpdateConnectionState,
|
ApiUpdateConnectionState,
|
||||||
ApiUpdateCurrentUser,
|
ApiUpdateCurrentUser,
|
||||||
|
ApiUpdatePasskeyOption,
|
||||||
ApiUpdateServerTimeOffset,
|
ApiUpdateServerTimeOffset,
|
||||||
ApiUpdateSession,
|
ApiUpdateSession,
|
||||||
|
ApiUpdateUserAlreadyAuthorized,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { LangCode } from '../../../types';
|
import type { LangCode } from '../../../types';
|
||||||
import type { RequiredGlobalActions } from '../../index';
|
import type { RequiredGlobalActions } from '../../index';
|
||||||
@ -13,6 +15,7 @@ import type { ActionReturnType, GlobalState } from '../../types';
|
|||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { getShippingError, shouldClosePaymentModal } from '../../../util/getReadableErrorText';
|
import { getShippingError, shouldClosePaymentModal } from '../../../util/getReadableErrorText';
|
||||||
import { unique } from '../../../util/iteratees';
|
import { unique } from '../../../util/iteratees';
|
||||||
|
import { getAccountsInfo, getAccountSlotUrl } from '../../../util/multiaccount';
|
||||||
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
||||||
import { clearWebTokenAuth } from '../../../util/routing';
|
import { clearWebTokenAuth } from '../../../util/routing';
|
||||||
import { setServerTimeOffset } from '../../../util/serverTime';
|
import { setServerTimeOffset } from '../../../util/serverTime';
|
||||||
@ -20,9 +23,10 @@ import { updateSessionUserId } from '../../../util/sessions';
|
|||||||
import { forceWebsync } from '../../../util/websync';
|
import { forceWebsync } from '../../../util/websync';
|
||||||
import { isChatChannel, isChatSuperGroup } from '../../helpers';
|
import { isChatChannel, isChatSuperGroup } from '../../helpers';
|
||||||
import {
|
import {
|
||||||
addActionHandler, getGlobal, setGlobal,
|
addActionHandler, getActions, getGlobal, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
import { updateUser, updateUserFullInfo } from '../../reducers';
|
import { updateUser, updateUserFullInfo } from '../../reducers';
|
||||||
|
import { updateAuth } from '../../reducers/auth';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
import { selectTabState } from '../../selectors';
|
import { selectTabState } from '../../selectors';
|
||||||
import { selectSharedSettings } from '../../selectors/sharedState';
|
import { selectSharedSettings } from '../../selectors/sharedState';
|
||||||
@ -41,10 +45,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
onUpdateAuthorizationError(global, update);
|
onUpdateAuthorizationError(global, update);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'updateUserAlreadyAuthorized':
|
||||||
|
onUpdateUserAlreadyAuthorized(global, update);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'updateWebAuthTokenFailed':
|
case 'updateWebAuthTokenFailed':
|
||||||
onUpdateWebAuthTokenFailed(global);
|
onUpdateWebAuthTokenFailed(global);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'updatePasskeyOption':
|
||||||
|
onUpdatePasskeyOption(global, update);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'updateConnectionState':
|
case 'updateConnectionState':
|
||||||
onUpdateConnectionState(global, actions, update);
|
onUpdateConnectionState(global, actions, update);
|
||||||
break;
|
break;
|
||||||
@ -116,58 +128,49 @@ function onUpdateApiReady<T extends GlobalState>(global: T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationState) {
|
function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationState) {
|
||||||
global = getGlobal();
|
const wasAuthReady = global.auth.state === 'authorizationStateReady';
|
||||||
|
|
||||||
const wasAuthReady = global.authState === 'authorizationStateReady';
|
|
||||||
const authState = update.authorizationState;
|
const authState = update.authorizationState;
|
||||||
|
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
state: authState,
|
||||||
authState,
|
isLoading: false,
|
||||||
authIsLoading: false,
|
});
|
||||||
};
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|
||||||
switch (authState) {
|
switch (authState) {
|
||||||
case 'authorizationStateLoggingOut':
|
case 'authorizationStateLoggingOut':
|
||||||
void forceWebsync(false);
|
void forceWebsync(false);
|
||||||
|
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
|
||||||
isLoggingOut: true,
|
isLoggingOut: true,
|
||||||
};
|
});
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
break;
|
break;
|
||||||
case 'authorizationStateWaitCode':
|
case 'authorizationStateWaitCode':
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
isCodeViaApp: update.isCodeViaApp,
|
||||||
authIsCodeViaApp: update.isCodeViaApp,
|
});
|
||||||
};
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
break;
|
break;
|
||||||
case 'authorizationStateWaitPassword':
|
case 'authorizationStateWaitPassword':
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
hint: update.hint,
|
||||||
authHint: update.hint,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if (update.noReset) {
|
if (update.noReset) {
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
|
||||||
hasWebAuthTokenPasswordRequired: true,
|
hasWebAuthTokenPasswordRequired: true,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
break;
|
break;
|
||||||
case 'authorizationStateWaitQrCode':
|
case 'authorizationStateWaitQrCode':
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
isLoadingQrCode: false,
|
||||||
authIsLoadingQrCode: false,
|
qrCode: update.qrCode,
|
||||||
authQrCode: update.qrCode,
|
});
|
||||||
};
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
break;
|
break;
|
||||||
case 'authorizationStateReady': {
|
case 'authorizationStateReady': {
|
||||||
@ -177,10 +180,9 @@ function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: Ap
|
|||||||
|
|
||||||
void forceWebsync(true);
|
void forceWebsync(true);
|
||||||
|
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
|
||||||
isLoggingOut: false,
|
isLoggingOut: false,
|
||||||
};
|
});
|
||||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||||
global = updateTabState(global, {
|
global = updateTabState(global, {
|
||||||
inactiveReason: undefined,
|
inactiveReason: undefined,
|
||||||
@ -194,22 +196,44 @@ function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: Ap
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateAuthorizationError<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationError) {
|
function onUpdateAuthorizationError<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationError) {
|
||||||
// TODO: Investigate why TS is not happy with spread for lang related types
|
if (update.errorCode === 'PASSKEY_CREDENTIAL_NOT_FOUND') {
|
||||||
global = {
|
getActions().showNotification({
|
||||||
...global,
|
message: update.errorKey,
|
||||||
};
|
tabId: getCurrentTabId(),
|
||||||
global.authErrorKey = update.errorKey;
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = updateAuth(global, {
|
||||||
|
errorKey: update.errorKey,
|
||||||
|
});
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUpdateUserAlreadyAuthorized<T extends GlobalState>(global: T, update: ApiUpdateUserAlreadyAuthorized) {
|
||||||
|
const { userId } = update;
|
||||||
|
if (global.currentUserId === userId) return;
|
||||||
|
|
||||||
|
const accounts = getAccountsInfo();
|
||||||
|
const slot = Object.entries(accounts).find(([_, info]) => info.userId === userId)?.[0];
|
||||||
|
if (!slot) return;
|
||||||
|
const url = getAccountSlotUrl(Number(slot));
|
||||||
|
window.location.replace(url);
|
||||||
|
}
|
||||||
|
|
||||||
function onUpdateWebAuthTokenFailed<T extends GlobalState>(global: T) {
|
function onUpdateWebAuthTokenFailed<T extends GlobalState>(global: T) {
|
||||||
clearWebTokenAuth();
|
clearWebTokenAuth();
|
||||||
global = getGlobal();
|
|
||||||
|
|
||||||
global = {
|
global = updateAuth(global, {
|
||||||
...global,
|
|
||||||
hasWebAuthTokenFailed: true,
|
hasWebAuthTokenFailed: true,
|
||||||
};
|
});
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdatePasskeyOption<T extends GlobalState>(global: T, update: ApiUpdatePasskeyOption) {
|
||||||
|
global = updateAuth(global, {
|
||||||
|
passkeyOption: update.option,
|
||||||
|
});
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +242,6 @@ function onUpdateConnectionState<T extends GlobalState>(
|
|||||||
) {
|
) {
|
||||||
const { connectionState } = update;
|
const { connectionState } = update;
|
||||||
|
|
||||||
global = getGlobal();
|
|
||||||
const tabState = selectTabState(global, getCurrentTabId());
|
const tabState = selectTabState(global, getCurrentTabId());
|
||||||
if (connectionState === 'connectionStateReady' && tabState.isMasterTab && tabState.multitabNextAction) {
|
if (connectionState === 'connectionStateReady' && tabState.isMasterTab && tabState.multitabNextAction) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -258,7 +281,7 @@ function onUpdateConnectionState<T extends GlobalState>(
|
|||||||
|
|
||||||
function onUpdateSession<T extends GlobalState>(global: T, actions: RequiredGlobalActions, update: ApiUpdateSession) {
|
function onUpdateSession<T extends GlobalState>(global: T, actions: RequiredGlobalActions, update: ApiUpdateSession) {
|
||||||
const { sessionData } = update;
|
const { sessionData } = update;
|
||||||
const { authRememberMe, authState } = global;
|
const { rememberMe, state } = global.auth;
|
||||||
const isEmpty = !sessionData || !sessionData.mainDcId;
|
const isEmpty = !sessionData || !sessionData.mainDcId;
|
||||||
|
|
||||||
const isTest = sessionData?.isTest;
|
const isTest = sessionData?.isTest;
|
||||||
@ -273,7 +296,7 @@ function onUpdateSession<T extends GlobalState>(global: T, actions: RequiredGlob
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authRememberMe || authState !== 'authorizationStateReady' || isEmpty) {
|
if (!rememberMe || state !== 'authorizationStateReady' || isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { addCallback } from '../../../lib/teact/teactn';
|
import { addCallback } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import type { ApiNotification } from '../../../api/types';
|
||||||
import type { LangCode } from '../../../types';
|
import type { LangCode } from '../../../types';
|
||||||
import type { ActionReturnType, GlobalState } from '../../types';
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV, IS_WINDOWS,
|
IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV, IS_WINDOWS,
|
||||||
} from '../../../util/browser/windowEnvironment';
|
} from '../../../util/browser/windowEnvironment';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import generateUniqueId from '../../../util/generateUniqueId';
|
||||||
import { subscribe, unsubscribe } from '../../../util/notifications';
|
import { subscribe, unsubscribe } from '../../../util/notifications';
|
||||||
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
||||||
import { decryptSessionByCurrentHash } from '../../../util/passcode';
|
import { decryptSessionByCurrentHash } from '../../../util/passcode';
|
||||||
@ -22,6 +24,7 @@ import { callApi } from '../../../api/gramjs';
|
|||||||
import { clearCaching, setupCaching } from '../../cache';
|
import { clearCaching, setupCaching } from '../../cache';
|
||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
import { updateSharedSettings } from '../../reducers';
|
import { updateSharedSettings } from '../../reducers';
|
||||||
|
import { updateAuth } from '../../reducers/auth';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
import {
|
import {
|
||||||
selectCanAnimateInterface,
|
selectCanAnimateInterface,
|
||||||
@ -216,24 +219,21 @@ addActionHandler('setIsUiReady', (global, actions, payload): ActionReturnType =>
|
|||||||
addActionHandler('setAuthPhoneNumber', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setAuthPhoneNumber', (global, actions, payload): ActionReturnType => {
|
||||||
const { phoneNumber } = payload;
|
const { phoneNumber } = payload;
|
||||||
|
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
phoneNumber,
|
||||||
authPhoneNumber: phoneNumber,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('setAuthRememberMe', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setAuthRememberMe', (global, actions, payload): ActionReturnType => {
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
rememberMe: Boolean(payload.value),
|
||||||
authRememberMe: Boolean(payload.value),
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('clearAuthErrorKey', (global): ActionReturnType => {
|
addActionHandler('clearAuthErrorKey', (global): ActionReturnType => {
|
||||||
return {
|
return updateAuth(global, {
|
||||||
...global,
|
errorKey: undefined,
|
||||||
authErrorKey: undefined,
|
});
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('disableHistoryAnimations', (global, actions, payload): ActionReturnType => {
|
addActionHandler('disableHistoryAnimations', (global, actions, payload): ActionReturnType => {
|
||||||
@ -256,3 +256,33 @@ addActionHandler('disableHistoryAnimations', (global, actions, payload): ActionR
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
setGlobal(global, { forceSyncOnIOs: true });
|
setGlobal(global, { forceSyncOnIOs: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('showNotification', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId(), ...notification } = payload;
|
||||||
|
const hasLocalId = notification.localId;
|
||||||
|
notification.localId ||= generateUniqueId();
|
||||||
|
|
||||||
|
const newNotifications = [...selectTabState(global, tabId).notifications];
|
||||||
|
const existingNotificationIndex = newNotifications.findIndex((n) => (
|
||||||
|
hasLocalId ? n.localId === notification.localId : n.message === notification.message
|
||||||
|
));
|
||||||
|
if (existingNotificationIndex !== -1) {
|
||||||
|
newNotifications.splice(existingNotificationIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
newNotifications.push(notification as ApiNotification);
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
notifications: newNotifications,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('dismissNotification', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload;
|
||||||
|
const newNotifications = selectTabState(global, tabId)
|
||||||
|
.notifications.filter(({ localId }) => localId !== payload.localId);
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
notifications: newNotifications,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { addCallback } from '../../../lib/teact/teactn';
|
import { addCallback } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import type { ApiError, ApiNotification } from '../../../api/types';
|
import type { ApiError } from '../../../api/types';
|
||||||
import type { ActionReturnType, GlobalState } from '../../types';
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -12,7 +12,6 @@ import { IS_TAURI } from '../../../util/browser/globalEnvironment';
|
|||||||
import { IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
import { IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||||
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
|
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
|
||||||
import { getAllNotificationsCount } from '../../../util/folderManager';
|
import { getAllNotificationsCount } from '../../../util/folderManager';
|
||||||
import generateUniqueId from '../../../util/generateUniqueId';
|
|
||||||
import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded';
|
import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded';
|
||||||
import getReadableErrorText from '../../../util/getReadableErrorText';
|
import getReadableErrorText from '../../../util/getReadableErrorText';
|
||||||
import { compact, unique } from '../../../util/iteratees';
|
import { compact, unique } from '../../../util/iteratees';
|
||||||
@ -332,26 +331,6 @@ addActionHandler('reorderStickerSets', (global, actions, payload): ActionReturnT
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('showNotification', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tabId = getCurrentTabId(), ...notification } = payload;
|
|
||||||
const hasLocalId = notification.localId;
|
|
||||||
notification.localId ||= generateUniqueId();
|
|
||||||
|
|
||||||
const newNotifications = [...selectTabState(global, tabId).notifications];
|
|
||||||
const existingNotificationIndex = newNotifications.findIndex((n) => (
|
|
||||||
hasLocalId ? n.localId === notification.localId : n.message === notification.message
|
|
||||||
));
|
|
||||||
if (existingNotificationIndex !== -1) {
|
|
||||||
newNotifications.splice(existingNotificationIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
newNotifications.push(notification as ApiNotification);
|
|
||||||
|
|
||||||
return updateTabState(global, {
|
|
||||||
notifications: newNotifications,
|
|
||||||
}, tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('showAllowedMessageTypesNotification', (global, actions, payload): ActionReturnType => {
|
addActionHandler('showAllowedMessageTypesNotification', (global, actions, payload): ActionReturnType => {
|
||||||
const { chatId, messageListType, tabId = getCurrentTabId() } = payload;
|
const { chatId, messageListType, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
@ -405,16 +384,6 @@ addActionHandler('showAllowedMessageTypesNotification', (global, actions, payloa
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('dismissNotification', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tabId = getCurrentTabId() } = payload;
|
|
||||||
const newNotifications = selectTabState(global, tabId)
|
|
||||||
.notifications.filter(({ localId }) => localId !== payload.localId);
|
|
||||||
|
|
||||||
return updateTabState(global, {
|
|
||||||
notifications: newNotifications,
|
|
||||||
}, tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('showDialog', (global, actions, payload): ActionReturnType => {
|
addActionHandler('showDialog', (global, actions, payload): ActionReturnType => {
|
||||||
const { data, tabId = getCurrentTabId() } = payload;
|
const { data, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { applyPerformanceSettings } from '../../../util/perfomanceSettings';
|
|||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
import { updatePeerColors } from '../../../util/theme';
|
import { updatePeerColors } from '../../../util/theme';
|
||||||
import { callApi, setShouldEnableDebugLog } from '../../../api/gramjs';
|
import { callApi, setShouldEnableDebugLog } from '../../../api/gramjs';
|
||||||
|
import { addTabStateResetterAction } from '../../helpers/meta';
|
||||||
import {
|
import {
|
||||||
addActionHandler, getActions, setGlobal,
|
addActionHandler, getActions, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
@ -226,3 +227,12 @@ addActionHandler('closeShareChatFolderModal', (global, actions, payload): Action
|
|||||||
shareFolderScreen: undefined,
|
shareFolderScreen: undefined,
|
||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openPasskeyModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
return updateTabState(global, {
|
||||||
|
isPasskeyModalOpen: true,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addTabStateResetterAction('closePasskeyModal', 'isPasskeyModalOpen');
|
||||||
|
|||||||
@ -362,11 +362,16 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
cached.sharedState.settings.shouldWarnAboutFiles = true;
|
cached.sharedState.settings.shouldWarnAboutFiles = true;
|
||||||
untypedCached.sharedState.settings.shouldWarnAboutSvg = undefined;
|
untypedCached.sharedState.settings.shouldWarnAboutSvg = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.auth) {
|
||||||
|
cached.auth = initialState.auth;
|
||||||
|
cached.auth.rememberMe = untypedCached.rememberMe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache(force?: boolean) {
|
function updateCache(force?: boolean) {
|
||||||
const global = getGlobal();
|
const global = getGlobal();
|
||||||
if (isRemovingCache || !isCaching || global.isLoggingOut || (!force && getIsHeavyAnimating())) {
|
if (isRemovingCache || !isCaching || global.auth.isLoggingOut || (!force && getIsHeavyAnimating())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,10 +405,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
|
|||||||
...pick(global, [
|
...pick(global, [
|
||||||
'appConfig',
|
'appConfig',
|
||||||
'config',
|
'config',
|
||||||
'authState',
|
'auth',
|
||||||
'authPhoneNumber',
|
|
||||||
'authRememberMe',
|
|
||||||
'authNearestCountry',
|
|
||||||
'attachMenu',
|
'attachMenu',
|
||||||
'currentUserId',
|
'currentUserId',
|
||||||
'contactList',
|
'contactList',
|
||||||
|
|||||||
@ -110,7 +110,7 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
|
|||||||
|
|
||||||
const parsedMessageList = parseLocationHash(global.currentUserId);
|
const parsedMessageList = parseLocationHash(global.currentUserId);
|
||||||
|
|
||||||
if (global.authState !== 'authorizationStateReady'
|
if (global.auth.state !== 'authorizationStateReady'
|
||||||
&& !global.passcode.hasPasscode && !global.passcode.isScreenLocked) {
|
&& !global.passcode.hasPasscode && !global.passcode.isScreenLocked) {
|
||||||
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
||||||
if (otherTabId === tabId) return;
|
if (otherTabId === tabId) return;
|
||||||
|
|||||||
@ -113,7 +113,9 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
lastPlaybackRate: DEFAULT_PLAYBACK_RATE,
|
lastPlaybackRate: DEFAULT_PLAYBACK_RATE,
|
||||||
},
|
},
|
||||||
|
|
||||||
authRememberMe: true,
|
auth: {
|
||||||
|
rememberMe: true,
|
||||||
|
},
|
||||||
countryList: {
|
countryList: {
|
||||||
phoneCodes: [],
|
phoneCodes: [],
|
||||||
general: [],
|
general: [],
|
||||||
|
|||||||
15
src/global/reducers/auth.ts
Normal file
15
src/global/reducers/auth.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type {
|
||||||
|
GlobalState,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export function updateAuth<T extends GlobalState>(
|
||||||
|
global: T, update: Partial<T['auth']>,
|
||||||
|
): T {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
auth: {
|
||||||
|
...global.auth,
|
||||||
|
...update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -22,7 +22,7 @@ export function selectLanguageCode<T extends GlobalState>(global: T) {
|
|||||||
export function selectCanSetPasscode<T extends GlobalState>(global: T) {
|
export function selectCanSetPasscode<T extends GlobalState>(global: T) {
|
||||||
// TODO[passcode]: remove this when multiacc passcode is implemented
|
// TODO[passcode]: remove this when multiacc passcode is implemented
|
||||||
const accounts = getAccountsInfo();
|
const accounts = getAccountsInfo();
|
||||||
return global.authRememberMe && !ACCOUNT_SLOT && Object.keys(accounts).length === 1;
|
return global.auth.rememberMe && !ACCOUNT_SLOT && Object.keys(accounts).length === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectTranslationLanguage<T extends GlobalState>(global: T) {
|
export function selectTranslationLanguage<T extends GlobalState>(global: T) {
|
||||||
|
|||||||
@ -147,6 +147,7 @@ export interface ActionPayloads {
|
|||||||
tabId?: number;
|
tabId?: number;
|
||||||
};
|
};
|
||||||
goToAuthQrCode: undefined;
|
goToAuthQrCode: undefined;
|
||||||
|
loginWithPasskey: undefined;
|
||||||
|
|
||||||
// stickers & GIFs
|
// stickers & GIFs
|
||||||
setStickerSearchQuery: { query?: string } & WithTabId;
|
setStickerSearchQuery: { query?: string } & WithTabId;
|
||||||
@ -250,6 +251,15 @@ export interface ActionPayloads {
|
|||||||
notificationSoundVolume?: number;
|
notificationSoundVolume?: number;
|
||||||
};
|
};
|
||||||
loadLanguages: undefined;
|
loadLanguages: undefined;
|
||||||
|
|
||||||
|
loadPasskeys: undefined;
|
||||||
|
startPasskeyRegistration: WithTabId | undefined;
|
||||||
|
deletePasskey: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
openPasskeyModal: WithTabId | undefined;
|
||||||
|
closePasskeyModal: WithTabId | undefined;
|
||||||
|
|
||||||
loadPrivacySettings: {
|
loadPrivacySettings: {
|
||||||
skipIfCached?: boolean;
|
skipIfCached?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import type {
|
|||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiNotifyPeerType,
|
ApiNotifyPeerType,
|
||||||
ApiPaidReactionPrivacyType,
|
ApiPaidReactionPrivacyType,
|
||||||
|
ApiPasskey,
|
||||||
|
ApiPasskeyOption,
|
||||||
ApiPeerColors,
|
ApiPeerColors,
|
||||||
ApiPeerNotifySettings,
|
ApiPeerNotifySettings,
|
||||||
ApiPeerPhotos,
|
ApiPeerPhotos,
|
||||||
@ -88,8 +90,6 @@ export type GlobalState = {
|
|||||||
byId: Record<string, ApiTimezone>;
|
byId: Record<string, ApiTimezone>;
|
||||||
hash: number;
|
hash: number;
|
||||||
};
|
};
|
||||||
hasWebAuthTokenFailed?: boolean;
|
|
||||||
hasWebAuthTokenPasswordRequired?: true;
|
|
||||||
isCacheApiSupported?: boolean;
|
isCacheApiSupported?: boolean;
|
||||||
connectionState?: ApiUpdateConnectionStateType;
|
connectionState?: ApiUpdateConnectionStateType;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
@ -146,20 +146,25 @@ export type GlobalState = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO Move to `auth`.
|
auth: {
|
||||||
isLoggingOut?: boolean;
|
isLoggingOut?: boolean;
|
||||||
authState?: ApiUpdateAuthorizationStateType;
|
state?: ApiUpdateAuthorizationStateType;
|
||||||
authPhoneNumber?: string;
|
phoneNumber?: string;
|
||||||
authIsLoading?: boolean;
|
isLoading?: boolean;
|
||||||
authIsLoadingQrCode?: boolean;
|
isLoadingQrCode?: boolean;
|
||||||
authErrorKey?: RegularLangFnParameters;
|
errorKey?: RegularLangFnParameters;
|
||||||
authRememberMe?: boolean;
|
rememberMe?: boolean;
|
||||||
authNearestCountry?: string;
|
nearestCountry?: string;
|
||||||
authIsCodeViaApp?: boolean;
|
isCodeViaApp?: boolean;
|
||||||
authHint?: string;
|
hint?: string;
|
||||||
authQrCode?: {
|
qrCode?: {
|
||||||
token: string;
|
token: string;
|
||||||
expires: number;
|
expires: number;
|
||||||
|
};
|
||||||
|
passkeyOption?: ApiPasskeyOption;
|
||||||
|
|
||||||
|
hasWebAuthTokenFailed?: true;
|
||||||
|
hasWebAuthTokenPasswordRequired?: true;
|
||||||
};
|
};
|
||||||
countryList: {
|
countryList: {
|
||||||
phoneCodes: ApiCountryCode[];
|
phoneCodes: ApiCountryCode[];
|
||||||
@ -437,6 +442,7 @@ export type GlobalState = {
|
|||||||
botVerificationShownPeerIds: string[];
|
botVerificationShownPeerIds: string[];
|
||||||
themes: Partial<Record<ThemeKey, IThemeSettings>>;
|
themes: Partial<Record<ThemeKey, IThemeSettings>>;
|
||||||
accountDaysTtl: number;
|
accountDaysTtl: number;
|
||||||
|
passkeys?: ApiPasskey[];
|
||||||
};
|
};
|
||||||
|
|
||||||
push?: {
|
push?: {
|
||||||
|
|||||||
@ -920,6 +920,8 @@ export type TabState = {
|
|||||||
threadId?: ThreadId;
|
threadId?: ThreadId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isPasskeyModalOpen?: boolean;
|
||||||
|
|
||||||
isWaitingForStarGiftUpgrade?: true;
|
isWaitingForStarGiftUpgrade?: true;
|
||||||
isWaitingForStarGiftTransfer?: true;
|
isWaitingForStarGiftTransfer?: true;
|
||||||
insertingPeerIdMention?: string;
|
insertingPeerIdMention?: string;
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const REFRESH_INTERVAL = 1000 * 60;
|
|||||||
const PREVIEW_SIZE = 72;
|
const PREVIEW_SIZE = 72;
|
||||||
|
|
||||||
export default function useMultiaccountInfo(currentUser?: ApiUser) {
|
export default function useMultiaccountInfo(currentUser?: ApiUser) {
|
||||||
const isUpdater = Boolean(currentUser) && getGlobal().authRememberMe;
|
const isUpdater = Boolean(currentUser) && getGlobal().auth.rememberMe;
|
||||||
const isSynced = useSelector(selectIsSynced);
|
const isSynced = useSelector(selectIsSynced);
|
||||||
|
|
||||||
const [accountsInfo, setAccountsInfo] = useState(() => getAccountsInfo());
|
const [accountsInfo, setAccountsInfo] = useState(() => getAccountsInfo());
|
||||||
|
|||||||
@ -78,7 +78,7 @@ async function init() {
|
|||||||
getActions()
|
getActions()
|
||||||
.switchMultitabRole({ isMasterTab }, { forceSyncOnIOs: true });
|
.switchMultitabRole({ isMasterTab }, { forceSyncOnIOs: true });
|
||||||
});
|
});
|
||||||
const shouldReestablishMasterToSelf = getGlobal().authState !== 'authorizationStateReady';
|
const shouldReestablishMasterToSelf = getGlobal().auth.state !== 'authorizationStateReady';
|
||||||
establishMultitabRole(shouldReestablishMasterToSelf);
|
establishMultitabRole(shouldReestablishMasterToSelf);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|||||||
@ -130,7 +130,7 @@ export async function generateKeyDataFromNonce(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertToLittle(buf: Uint32Array) {
|
export function convertToLittle(buf: Uint32Array) {
|
||||||
const correct = Buffer.alloc(buf.length * 4);
|
const correct = Buffer.allocUnsafe(buf.length * 4);
|
||||||
|
|
||||||
for (let i = 0; i < buf.length; i++) {
|
for (let i = 0; i < buf.length; i++) {
|
||||||
correct.writeUInt32BE(buf[i], i * 4);
|
correct.writeUInt32BE(buf[i], i * 4);
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
|
import type { ApiPasskeyOption } from '../../../api/types';
|
||||||
import type TelegramClient from './TelegramClient';
|
import type TelegramClient from './TelegramClient';
|
||||||
import type { Update } from './TelegramClient';
|
import type { Update } from './TelegramClient';
|
||||||
|
|
||||||
|
import { DEBUG } from '../../../config';
|
||||||
|
import { base64UrlToString } from '../../../util/encoding/base64';
|
||||||
import { tryParseBigInt } from '../../../util/numbers';
|
import { tryParseBigInt } from '../../../util/numbers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { DEFAULT_PRIMITIVES } from '../../../api/gramjs/gramjsBuilders';
|
import { DEFAULT_PRIMITIVES } from '../../../api/gramjs/gramjsBuilders';
|
||||||
import { RPCError } from '../errors';
|
import { buildInputPasskeyCredential } from '../../../api/gramjs/gramjsBuilders/passkeys';
|
||||||
|
import {
|
||||||
|
PasskeyCredentialNotFoundError, PasskeyLoginRequestedError, RPCError, UserAlreadyAuthorizedError,
|
||||||
|
} from '../errors';
|
||||||
import Api from '../tl/api';
|
import Api from '../tl/api';
|
||||||
|
|
||||||
import { sleep } from '../Helpers';
|
import { sleep } from '../Helpers';
|
||||||
@ -14,6 +20,7 @@ import { getDisplayName } from '../Utils';
|
|||||||
export interface UserAuthParams {
|
export interface UserAuthParams {
|
||||||
phoneNumber: string | (() => Promise<string>);
|
phoneNumber: string | (() => Promise<string>);
|
||||||
webAuthTokenFailed: () => void;
|
webAuthTokenFailed: () => void;
|
||||||
|
onPasskeyOption: (passkeyOption: ApiPasskeyOption) => void;
|
||||||
phoneCode: (isCodeViaApp?: boolean) => Promise<string>;
|
phoneCode: (isCodeViaApp?: boolean) => Promise<string>;
|
||||||
password: (hint?: string, noReset?: boolean) => Promise<string>;
|
password: (hint?: string, noReset?: boolean) => Promise<string>;
|
||||||
firstAndLastNames: () => Promise<[string, string?]>;
|
firstAndLastNames: () => Promise<[string, string?]>;
|
||||||
@ -24,6 +31,7 @@ export interface UserAuthParams {
|
|||||||
shouldThrowIfUnauthorized?: boolean;
|
shouldThrowIfUnauthorized?: boolean;
|
||||||
webAuthToken?: string;
|
webAuthToken?: string;
|
||||||
accountIds?: string[];
|
accountIds?: string[];
|
||||||
|
hasPasskeySupport?: boolean;
|
||||||
mockScenario?: string;
|
mockScenario?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +44,9 @@ interface ApiCredentials {
|
|||||||
apiHash: string;
|
apiHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_INITIAL_METHOD = 'phoneNumber';
|
type AuthMethod = 'phoneNumber' | 'qrCode';
|
||||||
|
const DEFAULT_INITIAL_METHOD: AuthMethod = 'phoneNumber';
|
||||||
|
let lastUsedMethod: AuthMethod = DEFAULT_INITIAL_METHOD;
|
||||||
|
|
||||||
export async function authFlow(
|
export async function authFlow(
|
||||||
client: TelegramClient,
|
client: TelegramClient,
|
||||||
@ -61,6 +71,8 @@ export function signInUserWithPreferredMethod(
|
|||||||
): Promise<Api.TypeUser> {
|
): Promise<Api.TypeUser> {
|
||||||
const { initialMethod = DEFAULT_INITIAL_METHOD } = authParams;
|
const { initialMethod = DEFAULT_INITIAL_METHOD } = authParams;
|
||||||
|
|
||||||
|
refreshPasskeyLoginOption(client, apiCredentials, authParams);
|
||||||
|
|
||||||
if (initialMethod === 'phoneNumber') {
|
if (initialMethod === 'phoneNumber') {
|
||||||
return signInUser(client, apiCredentials, authParams);
|
return signInUser(client, apiCredentials, authParams);
|
||||||
} else {
|
} else {
|
||||||
@ -68,6 +80,22 @@ export function signInUserWithPreferredMethod(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshPasskeyLoginOption(
|
||||||
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams,
|
||||||
|
) {
|
||||||
|
if (!authParams.hasPasskeySupport) return;
|
||||||
|
obtainPasskeyLoginOption(client, apiCredentials).then((passkeyOption) => {
|
||||||
|
if (passkeyOption) {
|
||||||
|
authParams.onPasskeyOption(passkeyOption);
|
||||||
|
}
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to obtain passkey login option:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkAuthorization(client: TelegramClient, shouldThrow = false) {
|
export async function checkAuthorization(client: TelegramClient, shouldThrow = false) {
|
||||||
try {
|
try {
|
||||||
await client.invoke(new Api.updates.GetState());
|
await client.invoke(new Api.updates.GetState());
|
||||||
@ -114,6 +142,7 @@ async function signInUser(
|
|||||||
let phoneNumber;
|
let phoneNumber;
|
||||||
let phoneCodeHash;
|
let phoneCodeHash;
|
||||||
let isCodeViaApp = false;
|
let isCodeViaApp = false;
|
||||||
|
lastUsedMethod = 'phoneNumber';
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
@ -122,7 +151,11 @@ async function signInUser(
|
|||||||
phoneNumber = await authParams.phoneNumber();
|
phoneNumber = await authParams.phoneNumber();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error && err.message === 'RESTART_AUTH_WITH_QR') {
|
if (err instanceof Error && err.message === 'RESTART_AUTH_WITH_QR') {
|
||||||
return signInUserWithQrCode(client, apiCredentials, authParams);
|
return await signInUserWithQrCode(client, apiCredentials, authParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof PasskeyLoginRequestedError) {
|
||||||
|
return await signInUserWithPasskey(client, apiCredentials, authParams, err.credentialJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
@ -235,6 +268,8 @@ async function signInUserWithQrCode(
|
|||||||
const { apiId, apiHash } = apiCredentials;
|
const { apiId, apiHash } = apiCredentials;
|
||||||
const exceptIds = authParams.accountIds?.map((id) => tryParseBigInt(id)).filter(Boolean) || [];
|
const exceptIds = authParams.accountIds?.map((id) => tryParseBigInt(id)).filter(Boolean) || [];
|
||||||
|
|
||||||
|
lastUsedMethod = 'qrCode';
|
||||||
|
|
||||||
const inputPromise = (async () => {
|
const inputPromise = (async () => {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (1) {
|
while (1) {
|
||||||
@ -277,6 +312,10 @@ async function signInUserWithQrCode(
|
|||||||
return await signInUser(client, apiCredentials, authParams);
|
return await signInUser(client, apiCredentials, authParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err instanceof PasskeyLoginRequestedError) {
|
||||||
|
return await signInUserWithPasskey(client, apiCredentials, authParams, err.credentialJson);
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
isScanningComplete = true;
|
isScanningComplete = true;
|
||||||
@ -315,6 +354,102 @@ async function signInUserWithQrCode(
|
|||||||
throw undefined;
|
throw undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function obtainPasskeyLoginOption(
|
||||||
|
client: TelegramClient, apiCredentials: ApiCredentials,
|
||||||
|
): Promise<ApiPasskeyOption | undefined> {
|
||||||
|
const { apiId, apiHash } = apiCredentials;
|
||||||
|
const passkeyLoginOptions = await client.invoke(new Api.auth.InitPasskeyLogin({
|
||||||
|
apiId,
|
||||||
|
apiHash,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!passkeyLoginOptions) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(passkeyLoginOptions.options.data) as ApiPasskeyOption;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Failed to parse passkey login options:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signInUserWithPasskey(
|
||||||
|
client: TelegramClient,
|
||||||
|
apiCredentials: ApiCredentials,
|
||||||
|
authParams: UserAuthParams,
|
||||||
|
credentialJson: PublicKeyCredentialJSON,
|
||||||
|
): Promise<Api.TypeUser> {
|
||||||
|
try {
|
||||||
|
if (!credentialJson.response.userHandle) {
|
||||||
|
throw new Error('User handle is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userHandle = base64UrlToString(credentialJson.response.userHandle);
|
||||||
|
const [userDcIdStr, userId] = userHandle.split(':');
|
||||||
|
if (!userDcIdStr || !userId || isNaN(Number(userDcIdStr))) {
|
||||||
|
throw new Error('Unexpected user handle format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDcId = Number(userDcIdStr);
|
||||||
|
if (authParams.accountIds?.includes(userId)) {
|
||||||
|
throw new UserAlreadyAuthorizedError(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromDcId = client.session.dcId;
|
||||||
|
const unsignedFromAuthKeyId = client.session.getAuthKey(fromDcId).keyId; // Auth key id bytes are stored as unsigned 64-bit integer, long is signed
|
||||||
|
const signedFromAuthKeyId = unsignedFromAuthKeyId ? BigInt.asIntN(64, unsignedFromAuthKeyId) : undefined;
|
||||||
|
|
||||||
|
const isSwitchingDc = fromDcId !== userDcId;
|
||||||
|
if (isSwitchingDc) {
|
||||||
|
await client._switchDC(userDcId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.invoke(new Api.auth.FinishPasskeyLogin({
|
||||||
|
credential: buildInputPasskeyCredential(credentialJson),
|
||||||
|
fromDcId: isSwitchingDc ? fromDcId : undefined,
|
||||||
|
fromAuthKeyId: isSwitchingDc ? signedFromAuthKeyId : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (result instanceof Api.auth.Authorization) {
|
||||||
|
return result.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unexpected sign up in passkey login');
|
||||||
|
} catch (err: unknown) {
|
||||||
|
// We cannot call finishPasskeyLogin again with the same challenge
|
||||||
|
refreshPasskeyLoginOption(client, apiCredentials, authParams);
|
||||||
|
|
||||||
|
const isPasskeyUnknownError = err instanceof PasskeyCredentialNotFoundError;
|
||||||
|
|
||||||
|
if (isPasskeyUnknownError) {
|
||||||
|
authParams.onError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.message === 'RESTART_AUTH' || (isPasskeyUnknownError && lastUsedMethod === 'phoneNumber')) {
|
||||||
|
return signInUser(client, apiCredentials, authParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message === 'RESTART_AUTH_WITH_QR' || (isPasskeyUnknownError && lastUsedMethod === 'qrCode')) {
|
||||||
|
return signInUserWithQrCode(client, apiCredentials, authParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof RPCError && err.errorMessage === 'SESSION_PASSWORD_NEEDED') {
|
||||||
|
return signInWithPassword(client, apiCredentials, authParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof Error) {
|
||||||
|
authParams.onError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendCode(
|
async function sendCode(
|
||||||
client: TelegramClient, apiCredentials: ApiCredentials, phoneNumber: string, forceSMS = false,
|
client: TelegramClient, apiCredentials: ApiCredentials, phoneNumber: string, forceSMS = false,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
|
|||||||
@ -148,6 +148,31 @@ export class PasswordFreshError extends BadRequestError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PasskeyLoginRequestedError extends Error {
|
||||||
|
public credentialJson: PublicKeyCredentialJSON;
|
||||||
|
|
||||||
|
constructor(credentialJson: PublicKeyCredentialJSON) {
|
||||||
|
super('Passkey login requested');
|
||||||
|
this.message = 'RESTART_AUTH_WITH_PASSKEY';
|
||||||
|
this.credentialJson = credentialJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserAlreadyAuthorizedError extends Error {
|
||||||
|
public userId: string;
|
||||||
|
constructor(userId: string) {
|
||||||
|
super('User already authorized');
|
||||||
|
this.message = 'USER_ALREADY_AUTHORIZED';
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PasskeyCredentialNotFoundError extends RPCError {
|
||||||
|
constructor(args: any) {
|
||||||
|
super('PASSKEY_CREDENTIAL_NOT_FOUND', args.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const rpcErrorRe = new Map<RegExp, any>([
|
export const rpcErrorRe = new Map<RegExp, any>([
|
||||||
[/FILE_MIGRATE_(\d+)/, FileMigrateError],
|
[/FILE_MIGRATE_(\d+)/, FileMigrateError],
|
||||||
[/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError],
|
[/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError],
|
||||||
@ -161,4 +186,5 @@ export const rpcErrorRe = new Map<RegExp, any>([
|
|||||||
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
||||||
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
|
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
|
||||||
[/^Timeout$/, TimedOutError],
|
[/^Timeout$/, TimedOutError],
|
||||||
|
[/PASSKEY_CREDENTIAL_NOT_FOUND/, PasskeyCredentialNotFoundError],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -548,8 +548,9 @@ export default class MTProtoSender {
|
|||||||
try {
|
try {
|
||||||
await this._fallbackConnection?.send(data);
|
await this._fallbackConnection?.send(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._log.error(e);
|
|
||||||
this._log.info('Connection closed while sending data');
|
this._log.info('Connection closed while sending data');
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
this._longPollLoopHandle = undefined;
|
this._longPollLoopHandle = undefined;
|
||||||
this.isSendingLongPoll = false;
|
this.isSendingLongPoll = false;
|
||||||
if (!this.userDisconnected) {
|
if (!this.userDisconnected) {
|
||||||
@ -651,8 +652,9 @@ export default class MTProtoSender {
|
|||||||
await this.getConnection()!.send(data);
|
await this.getConnection()!.send(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.logWithIndex.debug(`Connection closed while sending data ${e}`);
|
this.logWithIndex.debug(`Connection closed while sending data ${e}`);
|
||||||
this._log.error(e);
|
|
||||||
this._log.info('Connection closed while sending data');
|
this._log.info('Connection closed while sending data');
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
this._sendLoopHandle = undefined;
|
this._sendLoopHandle = undefined;
|
||||||
if (!this.userDisconnected) {
|
if (!this.userDisconnected) {
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
@ -694,8 +696,9 @@ export default class MTProtoSender {
|
|||||||
// this._log.info('Connection closed while receiving data');
|
// this._log.info('Connection closed while receiving data');
|
||||||
/** when the server disconnects us we want to reconnect */
|
/** when the server disconnects us we want to reconnect */
|
||||||
if (!this.userDisconnected) {
|
if (!this.userDisconnected) {
|
||||||
this._log.error(e);
|
|
||||||
this._log.warn('Connection closed while receiving data');
|
this._log.warn('Connection closed while receiving data');
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
}
|
}
|
||||||
this._recvLoopHandle = undefined;
|
this._recvLoopHandle = undefined;
|
||||||
@ -731,7 +734,8 @@ export default class MTProtoSender {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this._log.error('Unhandled error while receiving data');
|
this._log.error('Unhandled error while receiving data');
|
||||||
this._log.error(e);
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
this.reconnect();
|
this.reconnect();
|
||||||
this._recvLoopHandle = undefined;
|
this._recvLoopHandle = undefined;
|
||||||
return;
|
return;
|
||||||
@ -750,7 +754,8 @@ export default class MTProtoSender {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._log.error('Unhandled error while receiving data');
|
this._log.error('Unhandled error while receiving data');
|
||||||
this._log.error(e);
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1544,6 +1544,8 @@ auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;
|
|||||||
auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector<long> = auth.LoginToken;
|
auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector<long> = auth.LoginToken;
|
||||||
auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken;
|
auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken;
|
||||||
auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization;
|
auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization;
|
||||||
|
auth.initPasskeyLogin#518ad0b7 api_id:int api_hash:string = auth.PasskeyLoginOptions;
|
||||||
|
auth.finishPasskeyLogin#9857ad07 flags:# credential:InputPasskeyCredential from_dc_id:flags.0?int from_auth_key_id:flags.0?long = auth.Authorization;
|
||||||
account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector<long> = Bool;
|
account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector<long> = Bool;
|
||||||
account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector<long> = Bool;
|
account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector<long> = Bool;
|
||||||
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
|
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
|
||||||
@ -1593,6 +1595,10 @@ account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
|
|||||||
account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;
|
account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;
|
||||||
account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;
|
account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;
|
||||||
account.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool;
|
account.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool;
|
||||||
|
account.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions;
|
||||||
|
account.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey;
|
||||||
|
account.getPasskeys#ea1f0c52 = account.Passkeys;
|
||||||
|
account.deletePasskey#f5b5563f id:string = Bool;
|
||||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||||
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
|
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
|
||||||
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;
|
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
"auth.dropTempAuthKeys",
|
"auth.dropTempAuthKeys",
|
||||||
"auth.exportLoginToken",
|
"auth.exportLoginToken",
|
||||||
"auth.importLoginToken",
|
"auth.importLoginToken",
|
||||||
|
"auth.initPasskeyLogin",
|
||||||
|
"auth.finishPasskeyLogin",
|
||||||
"account.registerDevice",
|
"account.registerDevice",
|
||||||
"account.unregisterDevice",
|
"account.unregisterDevice",
|
||||||
"account.updateNotifySettings",
|
"account.updateNotifySettings",
|
||||||
@ -68,6 +70,10 @@
|
|||||||
"account.getAccountTTL",
|
"account.getAccountTTL",
|
||||||
"account.setAccountTTL",
|
"account.setAccountTTL",
|
||||||
"account.setMainProfileTab",
|
"account.setMainProfileTab",
|
||||||
|
"account.initPasskeyRegistration",
|
||||||
|
"account.registerPasskey",
|
||||||
|
"account.getPasskeys",
|
||||||
|
"account.deletePasskey",
|
||||||
"users.getUsers",
|
"users.getUsers",
|
||||||
"users.getFullUser",
|
"users.getFullUser",
|
||||||
"contacts.getContacts",
|
"contacts.getContacts",
|
||||||
|
|||||||
@ -158,4 +158,6 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
|
|||||||
'translations.telegram.org',
|
'translations.telegram.org',
|
||||||
],
|
],
|
||||||
typingDraftTtl: 10,
|
typingDraftTtl: 10,
|
||||||
|
arePasskeysAvailable: true,
|
||||||
|
passkeysMaxCount: 5,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -274,3 +274,9 @@
|
|||||||
.peer-color-count-3 {
|
.peer-color-count-3 {
|
||||||
@include mixins.peer-gradient(--bar-gradient, 3);
|
@include mixins.peer-gradient(--bar-gradient, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-column-centered {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@ -102,6 +102,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--color-interactive-buffered: rgba(var(--color-text-secondary-rgb), 0.25); // Overlays underlying inactive element
|
--color-interactive-buffered: rgba(var(--color-text-secondary-rgb), 0.25); // Overlays underlying inactive element
|
||||||
--color-interactive-element-hover: rgba(var(--color-text-secondary-rgb), 0.08);
|
--color-interactive-element-hover: rgba(var(--color-text-secondary-rgb), 0.08);
|
||||||
--color-composer-button: #{$color-text-secondary}CC;
|
--color-composer-button: #{$color-text-secondary}CC;
|
||||||
|
--color-toast-background: #202020CC;
|
||||||
|
|
||||||
--color-voice-transcribe-button: #e8f3ff;
|
--color-voice-transcribe-button: #e8f3ff;
|
||||||
--color-voice-transcribe-button-own: #cceebf;
|
--color-voice-transcribe-button-own: #cceebf;
|
||||||
@ -215,6 +216,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--border-radius-button: 1rem;
|
--border-radius-button: 1rem;
|
||||||
--border-radius-button-tiny: 0.875rem;
|
--border-radius-button-tiny: 0.875rem;
|
||||||
--border-radius-modal: 2rem;
|
--border-radius-modal: 2rem;
|
||||||
|
--border-radius-toast: 1rem;
|
||||||
--border-radius-default: 0.75rem;
|
--border-radius-default: 0.75rem;
|
||||||
--border-radius-default-small: 0.625rem;
|
--border-radius-default-small: 0.625rem;
|
||||||
--border-radius-default-tiny: 0.375rem;
|
--border-radius-default-tiny: 0.375rem;
|
||||||
|
|||||||
@ -73,5 +73,6 @@
|
|||||||
"--color-chat-username": ["#3C7EB0", "#E9EEF4"],
|
"--color-chat-username": ["#3C7EB0", "#E9EEF4"],
|
||||||
"--color-borders-read-story": ["#C4C9CC", "#737373"],
|
"--color-borders-read-story": ["#C4C9CC", "#737373"],
|
||||||
"--color-background-menu-separator": ["#0000001a", "#ffffff1a"],
|
"--color-background-menu-separator": ["#0000001a", "#ffffff1a"],
|
||||||
"--color-hover-overlay": ["#00000006", "#ffffff06"]
|
"--color-hover-overlay": ["#00000006", "#ffffff06"],
|
||||||
|
"--color-toast-background": ["#202020CC", "#000000CC"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -262,6 +262,7 @@ export enum SettingsScreens {
|
|||||||
CustomEmoji,
|
CustomEmoji,
|
||||||
DoNotTranslate,
|
DoNotTranslate,
|
||||||
FoldersShare,
|
FoldersShare,
|
||||||
|
Passkeys,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||||
|
|||||||
31
src/types/language.d.ts
vendored
31
src/types/language.d.ts
vendored
@ -157,6 +157,7 @@ export interface LangPair {
|
|||||||
'LoginQRHelp2': undefined;
|
'LoginQRHelp2': undefined;
|
||||||
'LoginQRHelp3': undefined;
|
'LoginQRHelp3': undefined;
|
||||||
'LoginQRCancel': undefined;
|
'LoginQRCancel': undefined;
|
||||||
|
'LoginPasskey': undefined;
|
||||||
'YourName': undefined;
|
'YourName': undefined;
|
||||||
'LoginRegisterDesc': undefined;
|
'LoginRegisterDesc': undefined;
|
||||||
'LoginRegisterFirstNamePlaceholder': undefined;
|
'LoginRegisterFirstNamePlaceholder': undefined;
|
||||||
@ -468,8 +469,6 @@ export interface LangPair {
|
|||||||
'P2PContacts': undefined;
|
'P2PContacts': undefined;
|
||||||
'P2PNobody': undefined;
|
'P2PNobody': undefined;
|
||||||
'PrivacySettingsWebSessions': undefined;
|
'PrivacySettingsWebSessions': undefined;
|
||||||
'PasswordOn': undefined;
|
|
||||||
'PasswordOff': undefined;
|
|
||||||
'PrivacyTitle': undefined;
|
'PrivacyTitle': undefined;
|
||||||
'PrivacyPhoneTitle': undefined;
|
'PrivacyPhoneTitle': undefined;
|
||||||
'LastSeenTitle': undefined;
|
'LastSeenTitle': undefined;
|
||||||
@ -634,6 +633,7 @@ export interface LangPair {
|
|||||||
'ErrorNewSaltInvalid': undefined;
|
'ErrorNewSaltInvalid': undefined;
|
||||||
'ErrorPasswordChanged': undefined;
|
'ErrorPasswordChanged': undefined;
|
||||||
'ErrorPasswordMissing': undefined;
|
'ErrorPasswordMissing': undefined;
|
||||||
|
'ErrorPasskeyUnknown': undefined;
|
||||||
'ErrorUnspecified': undefined;
|
'ErrorUnspecified': undefined;
|
||||||
'NoStickers': undefined;
|
'NoStickers': undefined;
|
||||||
'ClearRecentEmoji': undefined;
|
'ClearRecentEmoji': undefined;
|
||||||
@ -1788,6 +1788,27 @@ export interface LangPair {
|
|||||||
'StarGiftPriceDecreaseInfoLink': undefined;
|
'StarGiftPriceDecreaseInfoLink': undefined;
|
||||||
'StarGiftUpgradeCostModalTitle': undefined;
|
'StarGiftUpgradeCostModalTitle': undefined;
|
||||||
'StarGiftUpgradeCostHint': undefined;
|
'StarGiftUpgradeCostHint': undefined;
|
||||||
|
'SettingsItemPrivacyPasskeys': undefined;
|
||||||
|
'SettingsItemPrivacyOn': undefined;
|
||||||
|
'SettingsItemPrivacyOff': undefined;
|
||||||
|
'SettingsPasskeyTitle': undefined;
|
||||||
|
'SettingsPasskeyInfo': undefined;
|
||||||
|
'SettingsPasskeyFallbackTitle': undefined;
|
||||||
|
'SettingsPasskeysFooterLink': undefined;
|
||||||
|
'SettingsPasskeysCreate': undefined;
|
||||||
|
'PasskeyModalTitle': undefined;
|
||||||
|
'PasskeyModalDescription': undefined;
|
||||||
|
'PasskeyModalFeature1Title': undefined;
|
||||||
|
'PasskeyModalFeature1Description': undefined;
|
||||||
|
'PasskeyModalFeature2Title': undefined;
|
||||||
|
'PasskeyModalFeature2Description': undefined;
|
||||||
|
'PasskeyModalFeature3Title': undefined;
|
||||||
|
'PasskeyModalFeature3Description': undefined;
|
||||||
|
'PasskeyModalButtonText': undefined;
|
||||||
|
'PasskeyDeleteTitle': undefined;
|
||||||
|
'PasskeyDeleteText': undefined;
|
||||||
|
'PasskeyCreateError': undefined;
|
||||||
|
'PasskeyLoginError': undefined;
|
||||||
'UnconfirmedAuthDeniedTitle': undefined;
|
'UnconfirmedAuthDeniedTitle': undefined;
|
||||||
'UnconfirmedAuthTitle': undefined;
|
'UnconfirmedAuthTitle': undefined;
|
||||||
'UnconfirmedAuthConfirm': undefined;
|
'UnconfirmedAuthConfirm': undefined;
|
||||||
@ -3098,6 +3119,12 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'StarGiftPriceDecreaseTimer': {
|
'StarGiftPriceDecreaseTimer': {
|
||||||
'timer': V;
|
'timer': V;
|
||||||
};
|
};
|
||||||
|
'SettingsPasskeyUsedAt': {
|
||||||
|
'date': V;
|
||||||
|
};
|
||||||
|
'SettingsPasskeysFooter': {
|
||||||
|
'link': V;
|
||||||
|
};
|
||||||
'UnconfirmedAuthDeniedMessage': {
|
'UnconfirmedAuthDeniedMessage': {
|
||||||
'location': V;
|
'location': V;
|
||||||
};
|
};
|
||||||
|
|||||||
17
src/util/browser/passkeys.ts
Normal file
17
src/util/browser/passkeys.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { ApiPasskeyOption, ApiPasskeyRegistrationOption } from '../../api/types';
|
||||||
|
|
||||||
|
export function toCredentialCreationOptions(option: ApiPasskeyRegistrationOption): CredentialCreationOptions {
|
||||||
|
const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON(option.publicKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCredentialRequestOptions(option: ApiPasskeyOption): CredentialRequestOptions {
|
||||||
|
const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(option.publicKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -114,6 +114,8 @@ export const IS_TRANSLATION_SUPPORTED = !IS_TEST;
|
|||||||
export const IS_TRANSLATION_DETECTOR_SUPPORTED = 'LanguageDetector' in window;
|
export const IS_TRANSLATION_DETECTOR_SUPPORTED = 'LanguageDetector' in window;
|
||||||
export const IS_VIEW_TRANSITION_SUPPORTED = CSS.supports('view-transition-class: test')
|
export const IS_VIEW_TRANSITION_SUPPORTED = CSS.supports('view-transition-class: test')
|
||||||
&& !IS_FIREFOX; // https://bugzilla.mozilla.org/show_bug.cgi?id=1994547
|
&& !IS_FIREFOX; // https://bugzilla.mozilla.org/show_bug.cgi?id=1994547
|
||||||
|
export const IS_WEBAUTHN_SUPPORTED = navigator.credentials && window.PublicKeyCredential
|
||||||
|
&& 'parseCreationOptionsFromJSON' in PublicKeyCredential;
|
||||||
|
|
||||||
export const MESSAGE_LIST_SENSITIVE_AREA = 750;
|
export const MESSAGE_LIST_SENSITIVE_AREA = 750;
|
||||||
|
|
||||||
|
|||||||
16
src/util/encoding/base64.ts
Normal file
16
src/util/encoding/base64.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export function base64UrlToBase64(base64Url: string): string {
|
||||||
|
const base64Encoded = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const padding = base64Url.length % 4 === 0 ? '' : '='.repeat(4 - (base64Url.length % 4));
|
||||||
|
const base64WithPadding = base64Encoded + padding;
|
||||||
|
return base64WithPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64UrlToBuffer(base64Url: string): Buffer<ArrayBuffer> {
|
||||||
|
const base64 = base64UrlToBase64(base64Url);
|
||||||
|
return Buffer.from(base64, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64UrlToString(base64Url: string): string {
|
||||||
|
const buffer = base64UrlToBuffer(base64Url);
|
||||||
|
return buffer.toString('utf-8');
|
||||||
|
}
|
||||||
@ -253,7 +253,6 @@ export default {
|
|||||||
oneValue: '%1$d user',
|
oneValue: '%1$d user',
|
||||||
otherValue: '%1$d users',
|
otherValue: '%1$d users',
|
||||||
},
|
},
|
||||||
PasswordOn: 'On',
|
|
||||||
BlockedUsersInfo: 'Blocked users will not be able to contact you and will not see your Last Seen time.',
|
BlockedUsersInfo: 'Blocked users will not be able to contact you and will not see your Last Seen time.',
|
||||||
EnabledPasswordText: 'You have enabled Two-Step verification.\nYou\'ll need the password you set up here to log in to your Telegram account.',
|
EnabledPasswordText: 'You have enabled Two-Step verification.\nYou\'ll need the password you set up here to log in to your Telegram account.',
|
||||||
ChangePassword: 'Change Password',
|
ChangePassword: 'Change Password',
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export async function initGlobal(force: boolean = false, prevGlobal?: GlobalStat
|
|||||||
const initial = cloneDeep(INITIAL_GLOBAL_STATE);
|
const initial = cloneDeep(INITIAL_GLOBAL_STATE);
|
||||||
const cache = await loadCache(initial);
|
const cache = await loadCache(initial);
|
||||||
let global = cache || initial;
|
let global = cache || initial;
|
||||||
if (IS_MOCKED_CLIENT) global.authState = 'authorizationStateReady';
|
if (IS_MOCKED_CLIENT) global.auth.state = 'authorizationStateReady';
|
||||||
|
|
||||||
const { hasPasscode, isScreenLocked } = global.passcode;
|
const { hasPasscode, isScreenLocked } = global.passcode;
|
||||||
if (hasPasscode && !isScreenLocked) {
|
if (hasPasscode && !isScreenLocked) {
|
||||||
|
|||||||
@ -90,9 +90,9 @@ export function startWebsync() {
|
|||||||
const timeout = WEBSYNC_TIMEOUT - (currentTs - ts);
|
const timeout = WEBSYNC_TIMEOUT - (currentTs - ts);
|
||||||
|
|
||||||
lastTimeout = setTimeout(() => {
|
lastTimeout = setTimeout(() => {
|
||||||
const { authState } = getGlobal();
|
const { auth } = getGlobal();
|
||||||
|
|
||||||
const authed = authState === 'authorizationStateReady' || hasStoredSession();
|
const authed = auth.state === 'authorizationStateReady' || hasStoredSession();
|
||||||
forceWebsync(authed);
|
forceWebsync(authed);
|
||||||
}, Math.max(0, timeout * 1000));
|
}, Math.max(0, timeout * 1000));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ const {
|
|||||||
HEAD,
|
HEAD,
|
||||||
APP_ENV = 'production',
|
APP_ENV = 'production',
|
||||||
APP_MOCKED_CLIENT = '',
|
APP_MOCKED_CLIENT = '',
|
||||||
|
HTTPS_CERT_PATH = '',
|
||||||
|
HTTPS_KEY_PATH = '',
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
const DEFAULT_APP_TITLE = `Telegram${APP_ENV !== 'production' ? ' Beta' : ''}`;
|
const DEFAULT_APP_TITLE = `Telegram${APP_ENV !== 'production' ? ' Beta' : ''}`;
|
||||||
@ -57,6 +59,17 @@ export default function createConfig(
|
|||||||
_: any,
|
_: any,
|
||||||
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
||||||
): Configuration {
|
): Configuration {
|
||||||
|
let server: Required<Configuration>['devServer']['server'] = 'http';
|
||||||
|
if (HTTPS_CERT_PATH && HTTPS_KEY_PATH) {
|
||||||
|
server = {
|
||||||
|
type: 'https',
|
||||||
|
options: {
|
||||||
|
key: HTTPS_KEY_PATH,
|
||||||
|
cert: HTTPS_CERT_PATH,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
entry: './src/index.tsx',
|
entry: './src/index.tsx',
|
||||||
@ -70,6 +83,7 @@ export default function createConfig(
|
|||||||
client: {
|
client: {
|
||||||
overlay: false,
|
overlay: false,
|
||||||
},
|
},
|
||||||
|
server,
|
||||||
static: [
|
static: [
|
||||||
{
|
{
|
||||||
directory: path.resolve(__dirname, 'public'),
|
directory: path.resolve(__dirname, 'public'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user