Support updated OAuth (#6798)
This commit is contained in:
parent
6770fff857
commit
d101cdf0d3
@ -166,7 +166,10 @@ export function buildJson(json: GramJs.TypeJSONValue): any {
|
||||
|
||||
export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlAuthResult | undefined {
|
||||
if (result instanceof GramJs.UrlAuthResultRequest) {
|
||||
const { bot, domain, requestWriteAccess } = result;
|
||||
const {
|
||||
bot, domain, requestWriteAccess, requestPhoneNumber, browser, platform, ip, region, matchCodes,
|
||||
matchCodesFirst, userIdHint,
|
||||
} = result;
|
||||
const user = buildApiUser(bot);
|
||||
if (!user) return undefined;
|
||||
|
||||
@ -177,6 +180,14 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
|
||||
domain,
|
||||
shouldRequestWriteAccess: requestWriteAccess,
|
||||
bot: user,
|
||||
shouldRequestPhoneNumber: requestPhoneNumber,
|
||||
browser,
|
||||
platform,
|
||||
ip,
|
||||
region,
|
||||
matchCodes,
|
||||
matchCodesFirst,
|
||||
userIdHint: userIdHint?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
import { generateRandomBigInt } from '../../../lib/gramjs/Helpers';
|
||||
|
||||
import type {
|
||||
@ -8,6 +9,7 @@ import type {
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiPeer,
|
||||
ApiThemeParameters,
|
||||
ApiUrlAuthResult,
|
||||
ApiUser,
|
||||
} from '../../types';
|
||||
|
||||
@ -450,23 +452,11 @@ export async function requestBotUrlAuth({
|
||||
buttonId: number;
|
||||
messageId: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.RequestUrlAuth({
|
||||
return invokeUrlAuthRequest(new GramJs.messages.RequestUrlAuth({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
buttonId,
|
||||
msgId: messageId,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const authResult = buildApiUrlAuthResult(result);
|
||||
if (authResult?.type === 'request') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateUser',
|
||||
id: authResult.bot.id,
|
||||
user: authResult.bot,
|
||||
});
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
export async function acceptBotUrlAuth({
|
||||
@ -474,67 +464,71 @@ export async function acceptBotUrlAuth({
|
||||
messageId,
|
||||
buttonId,
|
||||
isWriteAllowed,
|
||||
wasPhoneShared,
|
||||
matchCode,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
buttonId: number;
|
||||
isWriteAllowed?: boolean;
|
||||
wasPhoneShared?: boolean;
|
||||
matchCode?: string;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.AcceptUrlAuth({
|
||||
return invokeUrlAuthRequest(new GramJs.messages.AcceptUrlAuth({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId: messageId,
|
||||
buttonId,
|
||||
writeAllowed: isWriteAllowed || undefined,
|
||||
sharePhoneNumber: wasPhoneShared || undefined,
|
||||
matchCode: matchCode || undefined,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const authResult = buildApiUrlAuthResult(result);
|
||||
if (authResult?.type === 'request') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateUser',
|
||||
id: authResult.bot.id,
|
||||
user: authResult.bot,
|
||||
});
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
export async function requestLinkUrlAuth({ url }: { url: string }) {
|
||||
const result = await invokeRequest(new GramJs.messages.RequestUrlAuth({
|
||||
return invokeUrlAuthRequest(new GramJs.messages.RequestUrlAuth({
|
||||
url,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const authResult = buildApiUrlAuthResult(result);
|
||||
if (authResult?.type === 'request') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateUser',
|
||||
id: authResult.bot.id,
|
||||
user: authResult.bot,
|
||||
});
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
export async function acceptLinkUrlAuth({ url, isWriteAllowed }: { url: string; isWriteAllowed?: boolean }) {
|
||||
const result = await invokeRequest(new GramJs.messages.AcceptUrlAuth({
|
||||
export async function acceptLinkUrlAuth({
|
||||
url, isWriteAllowed, wasPhoneShared, matchCode,
|
||||
}: {
|
||||
url: string;
|
||||
isWriteAllowed?: boolean;
|
||||
wasPhoneShared?: boolean;
|
||||
matchCode?: string;
|
||||
}) {
|
||||
return invokeUrlAuthRequest(new GramJs.messages.AcceptUrlAuth({
|
||||
url,
|
||||
writeAllowed: isWriteAllowed || undefined,
|
||||
sharePhoneNumber: wasPhoneShared || undefined,
|
||||
matchCode: matchCode || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const authResult = buildApiUrlAuthResult(result);
|
||||
if (authResult?.type === 'request') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateUser',
|
||||
id: authResult.bot.id,
|
||||
user: authResult.bot,
|
||||
export async function checkUrlAuthMatchCode({ url, matchCode }: { url: string; matchCode: string }) {
|
||||
try {
|
||||
const result = await invokeRequest(new GramJs.messages.CheckUrlAuthMatchCode({
|
||||
url,
|
||||
matchCode,
|
||||
}), {
|
||||
shouldThrow: true,
|
||||
});
|
||||
if (!result) return { type: 'unmatched' };
|
||||
|
||||
return { type: 'matched' };
|
||||
} catch (err) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'URL_EXPIRED') {
|
||||
return { type: 'expired' };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
export async function declineUrlAuth({ url }: { url: string }) {
|
||||
return invokeRequest(new GramJs.messages.DeclineUrlAuth({ url }), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchBotCanSendMessage({ bot }: { bot: ApiUser }) {
|
||||
@ -720,3 +714,27 @@ export async function fetchBotsRecommendations({ user }: { user: ApiChat }) {
|
||||
count: result instanceof GramJs.users.UsersSlice ? result.count : similarBots.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function invokeUrlAuthRequest(
|
||||
request: GramJs.messages.RequestUrlAuth | GramJs.messages.AcceptUrlAuth,
|
||||
): Promise<ApiUrlAuthResult | undefined> {
|
||||
try {
|
||||
const result = await invokeRequest(request, { shouldThrow: true });
|
||||
if (!result) return undefined;
|
||||
|
||||
const authResult = buildApiUrlAuthResult(result);
|
||||
if (authResult?.type === 'request') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateUser',
|
||||
id: authResult.bot.id,
|
||||
user: authResult.bot,
|
||||
});
|
||||
}
|
||||
return authResult;
|
||||
} catch (err) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'URL_EXPIRED') {
|
||||
return { type: 'expired' };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1106,13 +1106,19 @@ export type ApiSearchPostsFlood = {
|
||||
starsAmount: number;
|
||||
};
|
||||
|
||||
export type LinkContext = {
|
||||
export type LinkContextMessage = {
|
||||
type: 'message';
|
||||
threadId?: ThreadId;
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
export type LinkContextInner = {
|
||||
type: 'inner';
|
||||
};
|
||||
|
||||
export type LinkContext = LinkContextMessage | LinkContextInner;
|
||||
|
||||
export interface ApiTopic {
|
||||
id: number;
|
||||
isClosed?: boolean;
|
||||
|
||||
@ -348,11 +348,19 @@ export interface ApiEmojiInteraction {
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
type ApiUrlAuthResultRequest = {
|
||||
export type ApiUrlAuthResultRequest = {
|
||||
type: 'request';
|
||||
bot: ApiUser;
|
||||
domain: string;
|
||||
shouldRequestWriteAccess?: boolean;
|
||||
shouldRequestPhoneNumber?: boolean;
|
||||
browser?: string;
|
||||
platform?: string;
|
||||
ip?: string;
|
||||
region?: string;
|
||||
matchCodes?: string[];
|
||||
matchCodesFirst?: boolean;
|
||||
userIdHint?: string;
|
||||
};
|
||||
|
||||
type ApiUrlAuthResultAccepted = {
|
||||
@ -360,11 +368,19 @@ type ApiUrlAuthResultAccepted = {
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type ApiUrlAuthResultExpired = {
|
||||
type: 'expired';
|
||||
};
|
||||
|
||||
type ApiUrlAuthResultDefault = {
|
||||
type: 'default';
|
||||
};
|
||||
|
||||
export type ApiUrlAuthResult = ApiUrlAuthResultRequest | ApiUrlAuthResultAccepted | ApiUrlAuthResultDefault;
|
||||
export type ApiUrlAuthResult =
|
||||
ApiUrlAuthResultRequest
|
||||
| ApiUrlAuthResultAccepted
|
||||
| ApiUrlAuthResultExpired
|
||||
| ApiUrlAuthResultDefault;
|
||||
|
||||
export interface ApiCollectibleInfo {
|
||||
amount: number;
|
||||
|
||||
@ -611,8 +611,19 @@
|
||||
"OpenUrlTitle" = "Open Link";
|
||||
"OpenUrlText" = "Do you want to open **{url}**?";
|
||||
"OpenUrlConfirm" = "Open";
|
||||
"ConversationOpenBotLinkLogin" = "Log in to **{url}** as {user}";
|
||||
"ConversationOpenBotLinkAllowMessages" = "Allow **{bot}** to send me messages";
|
||||
"BotAuthTitle" = "Log in to {url}";
|
||||
"BotAuthSiteSubtitle" = "This site will receive your **name**, **username** and **profile photo**.";
|
||||
"BotAuthAllowMessages" = "Allow Messages";
|
||||
"BotAuthAllowMessagesInfo" = "This will allow **{bot}** to message you.";
|
||||
"BotAuthInfo" = "This login attempt came from the device above.";
|
||||
"BotAuthDevice" = "Device";
|
||||
"BotAuthSelectEmoji" = "Tap the emoji shown on your other device";
|
||||
"BotAuthPhoneNumber" = "Phone Number";
|
||||
"BotAuthPhoneNumberText" = "**{domain}** wants to access your phone number **{phone}**.";
|
||||
"BotAuthPhoneNumberQuestion" = "Allow access?";
|
||||
"BotAuthPhoneNumberAccept" = "Allow";
|
||||
"BotAuthPhoneNumberDeny" = "Deny";
|
||||
"BotAuthLogin" = "Log in";
|
||||
"BotWebViewOpenBot" = "Open Bot";
|
||||
"BotChatMiniAppOpen" = "Open";
|
||||
"WebAppReloadPage" = "Reload Page";
|
||||
@ -725,6 +736,7 @@
|
||||
"ErrorPasswordChanged" = "Password has been changed, please try again";
|
||||
"ErrorPasswordMissing" = "You must set 2FA password to use this feature";
|
||||
"ErrorPasskeyUnknown" = "This passkey is not assigned to any account";
|
||||
"ErrorUrlExpired" = "This link has expired";
|
||||
"ErrorUnspecified" = "Error";
|
||||
"NoStickers" = "No stickers yet";
|
||||
"ClearRecentEmoji" = "Clear recent emoji?";
|
||||
@ -2749,6 +2761,7 @@
|
||||
"ChatListAuctionView" = "View";
|
||||
"BotAuthSuccessTitle" = "Login Successful";
|
||||
"BotAuthSuccessText" = "You've successfully logged into **{url}**";
|
||||
"BotAuthSuccessTextNoPhone" = "You're now logged in to **{url}**, but you didn't grant access to your phone number.";
|
||||
"GiftPreviewSelectedTraits" = "Selected traits";
|
||||
"GiftPreviewCountModels_one" = "This collection features **{count}** unique model";
|
||||
"GiftPreviewCountModels_other" = "This collection features **{count}** unique models";
|
||||
|
||||
@ -426,7 +426,7 @@ const Main = ({
|
||||
|
||||
const parsedInitialLocationHash = parseInitialLocationHash();
|
||||
if (parsedInitialLocationHash?.tgaddr) {
|
||||
processDeepLink(decodeURIComponent(parsedInitialLocationHash.tgaddr));
|
||||
processDeepLink(decodeURIComponent(parsedInitialLocationHash.tgaddr), { type: 'inner' });
|
||||
}
|
||||
}, [isSynced]);
|
||||
|
||||
@ -434,7 +434,7 @@ const Main = ({
|
||||
try {
|
||||
const url = event.payload || '';
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
processDeepLink(decodedUrl);
|
||||
processDeepLink(decodedUrl, { type: 'inner' });
|
||||
} catch (e) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
71
src/components/modals/urlAuth/UrlAuthModal.module.scss
Normal file
71
src/components/modals/urlAuth/UrlAuthModal.module.scss
Normal file
@ -0,0 +1,71 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.textCenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 12rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.infoLabel, .note {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.allowMessages {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.infoValue {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.matchCodes {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.matchCodeButton {
|
||||
--custom-emoji-size: 2rem;
|
||||
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@ -1,121 +1,366 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo, useCallback, useEffect, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { ensureProtocol } from '../../../util/browser/url';
|
||||
import { selectAnimatedEmoji, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatPhoneNumber } from '../../../util/phoneNumber';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import { useShallowSelector } from '../../../hooks/data/useSelector';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import SafeLink from '../../common/SafeLink';
|
||||
import Button from '../../ui/Button';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
|
||||
import styles from './UrlAuthModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal?: TabState['urlAuth'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
bot?: ApiUser;
|
||||
currentUser?: ApiUser;
|
||||
};
|
||||
|
||||
const UrlAuthModal: FC<OwnProps & StateProps> = ({
|
||||
modal, currentUser,
|
||||
}) => {
|
||||
const { closeUrlAuthModal, acceptBotUrlAuth, acceptLinkUrlAuth } = getActions();
|
||||
const [isLoginChecked, setLoginChecked] = useState(true);
|
||||
const [isWriteAccessChecked, setWriteAccessChecked] = useState(true);
|
||||
const currentAuth = useCurrentOrPrev(modal, false);
|
||||
const { domain, botId, shouldRequestWriteAccess } = currentAuth?.request || {};
|
||||
const bot = botId ? getGlobal().users.byId[botId] : undefined;
|
||||
type AcceptParams = {
|
||||
wasPhoneShared?: boolean;
|
||||
matchCode?: string;
|
||||
};
|
||||
|
||||
const lang = useOldLang();
|
||||
type DialogState = 'closed' | 'match-confirm' | 'phone';
|
||||
const MATCH_CODE_EMOJI_SIZE = 2 * REM;
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
if (modal?.url && isLoginChecked) {
|
||||
const acceptAction = modal.button ? acceptBotUrlAuth : acceptLinkUrlAuth;
|
||||
acceptAction({
|
||||
isWriteAllowed: isWriteAccessChecked,
|
||||
});
|
||||
} else if (currentAuth?.url) {
|
||||
window.open(ensureProtocol(currentAuth.url), '_blank', 'noopener');
|
||||
}
|
||||
closeUrlAuthModal();
|
||||
}, [
|
||||
modal, isLoginChecked, closeUrlAuthModal, acceptBotUrlAuth, acceptLinkUrlAuth, isWriteAccessChecked, currentAuth,
|
||||
]);
|
||||
const UrlAuthModal = ({
|
||||
modal, bot, currentUser,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
acceptBotUrlAuth, acceptLinkUrlAuth, checkUrlAuthMatchCode, declineUrlAuth,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
closeUrlAuthModal();
|
||||
}, [closeUrlAuthModal]);
|
||||
const renderingModal = useCurrentOrPrev(modal, false);
|
||||
const currentBot = useCurrentOrPrev(bot, false);
|
||||
const modalRequest = modal?.request;
|
||||
const renderingRequest = renderingModal?.request;
|
||||
const confirmedMatchCode = renderingModal?.matchCode;
|
||||
const botName = getUserFullName(currentBot) || currentBot?.firstName;
|
||||
const isMatchCodePrecheckPending = Boolean(
|
||||
modalRequest?.matchCodesFirst && modalRequest.matchCodes?.length && !modal?.matchCode,
|
||||
);
|
||||
const isOpen = Boolean(modal?.url && modal?.request) && !isMatchCodePrecheckPending;
|
||||
|
||||
const handleLoginChecked = useCallback((value: boolean) => {
|
||||
setLoginChecked(value);
|
||||
setWriteAccessChecked(value);
|
||||
}, [setLoginChecked]);
|
||||
const [isWriteAccessChecked, setWriteAccessChecked] = useState(
|
||||
() => Boolean(modalRequest?.shouldRequestWriteAccess),
|
||||
);
|
||||
const [selectedMatchCode, setSelectedMatchCode] = useState<string | undefined>();
|
||||
const [dialogState, setDialogState] = useState<DialogState>('closed');
|
||||
const effectiveMatchCode = confirmedMatchCode || selectedMatchCode;
|
||||
const isMatchDialogOpen = isMatchCodePrecheckPending || dialogState === 'match-confirm';
|
||||
const isPhoneDialogOpen = dialogState === 'phone';
|
||||
const matchCodeEmojisSelector = useCallback((global: GlobalState) => {
|
||||
return renderingRequest?.matchCodes?.map((matchCode) => selectAnimatedEmoji(global, matchCode));
|
||||
}, [renderingRequest?.matchCodes]);
|
||||
const matchCodeEmojis = useShallowSelector(matchCodeEmojisSelector);
|
||||
|
||||
// Reset on re-open
|
||||
useEffect(() => {
|
||||
if (domain) {
|
||||
setLoginChecked(true);
|
||||
setWriteAccessChecked(Boolean(shouldRequestWriteAccess));
|
||||
if (!modalRequest) {
|
||||
setSelectedMatchCode(undefined);
|
||||
setDialogState('closed');
|
||||
return;
|
||||
}
|
||||
}, [shouldRequestWriteAccess, domain]);
|
||||
|
||||
setWriteAccessChecked(Boolean(modalRequest.shouldRequestWriteAccess));
|
||||
setSelectedMatchCode(undefined);
|
||||
setDialogState('closed');
|
||||
}, [modalRequest]);
|
||||
|
||||
const handleDismiss = useLastCallback(() => {
|
||||
setDialogState('closed');
|
||||
declineUrlAuth();
|
||||
});
|
||||
|
||||
const submitAuth = useLastCallback((params: AcceptParams = {}) => {
|
||||
const acceptAction = renderingModal?.button ? acceptBotUrlAuth : acceptLinkUrlAuth;
|
||||
acceptAction({
|
||||
isWriteAllowed: renderingRequest?.shouldRequestWriteAccess ? isWriteAccessChecked : undefined,
|
||||
wasPhoneShared: params.wasPhoneShared,
|
||||
matchCode: params.matchCode ?? effectiveMatchCode,
|
||||
});
|
||||
});
|
||||
|
||||
const handleConfirm = useLastCallback(() => {
|
||||
if (!renderingRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderingRequest.matchCodes?.length && !effectiveMatchCode) {
|
||||
setDialogState('match-confirm');
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderingRequest.shouldRequestPhoneNumber) {
|
||||
setDialogState('phone');
|
||||
return;
|
||||
}
|
||||
|
||||
submitAuth();
|
||||
});
|
||||
|
||||
const handleMatchDialogClose = useLastCallback(() => {
|
||||
setDialogState('closed');
|
||||
});
|
||||
|
||||
const handleMatchCodeSelect = useLastCallback((matchCode: string) => {
|
||||
if (isMatchCodePrecheckPending) {
|
||||
checkUrlAuthMatchCode({ matchCode });
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedMatchCode(matchCode);
|
||||
setDialogState('closed');
|
||||
|
||||
if (renderingRequest?.shouldRequestPhoneNumber) {
|
||||
setDialogState('phone');
|
||||
return;
|
||||
}
|
||||
|
||||
submitAuth({ matchCode });
|
||||
});
|
||||
|
||||
const handlePhoneDialogClose = useLastCallback(() => {
|
||||
setDialogState('closed');
|
||||
});
|
||||
|
||||
const handlePhoneDecision = useLastCallback((wasPhoneShared: boolean) => {
|
||||
setDialogState('closed');
|
||||
submitAuth({ wasPhoneShared });
|
||||
});
|
||||
|
||||
const handleTriggerWriteAccess = useLastCallback(() => {
|
||||
setWriteAccessChecked(!isWriteAccessChecked);
|
||||
});
|
||||
|
||||
if (!renderingRequest) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const shouldRenderSessionInfo = Boolean(renderingRequest.platform
|
||||
|| renderingRequest.browser || renderingRequest.ip || renderingRequest.region,
|
||||
);
|
||||
const requestDomain = renderingRequest.domain;
|
||||
const formattedPhoneNumber = currentUser?.phoneNumber ? `+${formatPhoneNumber(currentUser.phoneNumber)}` : undefined;
|
||||
const titleText = lang('BotAuthTitle', {
|
||||
url: <SafeLink url={requestDomain} text={requestDomain} />,
|
||||
}, {
|
||||
withNodes: true,
|
||||
});
|
||||
const descriptionText = lang('BotAuthSiteSubtitle', undefined, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
});
|
||||
|
||||
function renderPhoneDialogText() {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{lang('BotAuthPhoneNumberText', {
|
||||
domain: requestDomain,
|
||||
phone: formattedPhoneNumber || lang('Phone'),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</p>
|
||||
<p>{lang('BotAuthPhoneNumberQuestion')}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
isOpen={Boolean(modal?.url)}
|
||||
onClose={handleDismiss}
|
||||
title={lang('OpenUrlTitle')}
|
||||
confirmLabel={lang('OpenUrlTitle')}
|
||||
confirmHandler={handleOpen}
|
||||
>
|
||||
{renderText(lang('OpenUrlAlert2', currentAuth?.url), ['links'])}
|
||||
{domain && (
|
||||
<Checkbox
|
||||
className="dialog-checkbox"
|
||||
checked={isLoginChecked}
|
||||
label={(
|
||||
<>
|
||||
{renderText(
|
||||
lang('Conversation.OpenBotLinkLogin', [domain, getUserFullName(currentUser)]),
|
||||
['simple_markdown'],
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
onCheck={handleLoginChecked}
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
contentClassName={styles.content}
|
||||
className="tall"
|
||||
onClose={handleDismiss}
|
||||
hasAbsoluteCloseButton
|
||||
isSlim
|
||||
>
|
||||
<Avatar
|
||||
peer={currentBot}
|
||||
size={96}
|
||||
className={styles.center}
|
||||
/>
|
||||
)}
|
||||
{shouldRequestWriteAccess && (
|
||||
<Checkbox
|
||||
className="dialog-checkbox"
|
||||
checked={isWriteAccessChecked}
|
||||
label={(
|
||||
<>
|
||||
{renderText(
|
||||
lang('Conversation.OpenBotLinkAllowMessages', getUserFullName(bot)),
|
||||
['simple_markdown'],
|
||||
|
||||
<h2 className={buildClassName(styles.center, styles.title)} dir="auto">{titleText}</h2>
|
||||
|
||||
<span className={styles.textCenter}>{descriptionText}</span>
|
||||
|
||||
{shouldRenderSessionInfo && (
|
||||
<>
|
||||
<div className={styles.infoCard}>
|
||||
<span className={styles.infoLabel}>{lang('BotAuthDevice')}</span>
|
||||
<span className={styles.infoValue}>
|
||||
{[renderingRequest.platform, renderingRequest.browser].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
<span className={styles.infoLabel}>{lang('SessionPreviewIp')}</span>
|
||||
<span className={styles.infoValue}>{renderingRequest.ip}</span>
|
||||
<span className={styles.infoLabel}>{lang('SessionPreviewLocation')}</span>
|
||||
<span className={styles.infoValue}>{renderingRequest.region}</span>
|
||||
</div>
|
||||
|
||||
<span className={styles.note}>{lang('BotAuthInfo')}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{renderingRequest.shouldRequestWriteAccess && (
|
||||
<>
|
||||
<ListItem
|
||||
className={styles.allowMessages}
|
||||
onClick={handleTriggerWriteAccess}
|
||||
rightElement={(
|
||||
<Switcher
|
||||
id="url_auth_allow_messages"
|
||||
label={lang('BotAuthAllowMessages')}
|
||||
checked={isWriteAccessChecked}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
onCheck={setWriteAccessChecked}
|
||||
disabled={!isLoginChecked}
|
||||
/>
|
||||
)}
|
||||
</ConfirmDialog>
|
||||
>
|
||||
{lang('BotAuthAllowMessages')}
|
||||
</ListItem>
|
||||
{botName && (
|
||||
<span className={styles.note}>
|
||||
{lang(
|
||||
'BotAuthAllowMessagesInfo',
|
||||
{ bot: botName },
|
||||
{ withNodes: true, withMarkdown: true },
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={buildClassName(styles.actions, styles.center)}>
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
color="gray"
|
||||
isText
|
||||
fluid
|
||||
noForcedUpperCase
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{lang('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
color="primary"
|
||||
fluid
|
||||
noForcedUpperCase
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{lang('BotAuthLogin')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={isMatchDialogOpen}
|
||||
title={lang('BotAuthSelectEmoji')}
|
||||
onClose={isMatchCodePrecheckPending ? handleDismiss : handleMatchDialogClose}
|
||||
className={buildClassName('confirm', styles.matchDialog)}
|
||||
>
|
||||
<div className={styles.matchCodes}>
|
||||
{renderingRequest.matchCodes?.map((matchCode, index) => {
|
||||
const animatedMatchCodeEmoji = matchCodeEmojis?.[index];
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={matchCode}
|
||||
fluid
|
||||
color="adaptive"
|
||||
className={styles.matchCodeButton}
|
||||
onClick={() => handleMatchCodeSelect(matchCode)}
|
||||
>
|
||||
{animatedMatchCodeEmoji ? (
|
||||
<CustomEmoji
|
||||
sticker={animatedMatchCodeEmoji}
|
||||
size={MATCH_CODE_EMOJI_SIZE}
|
||||
/>
|
||||
) : renderText(matchCode)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.footnote}>
|
||||
{lang('BotAuthTitle', {
|
||||
url: <SafeLink url={requestDomain} text={requestDomain} />,
|
||||
}, {
|
||||
withNodes: true,
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
color="danger"
|
||||
className={styles.cancelButton}
|
||||
isText
|
||||
noForcedUpperCase
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{lang('Cancel')}
|
||||
</Button>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={isPhoneDialogOpen}
|
||||
title={lang('BotAuthPhoneNumber')}
|
||||
onClose={handlePhoneDialogClose}
|
||||
className={buildClassName('confirm', styles.phoneDialog)}
|
||||
>
|
||||
{renderPhoneDialogText()}
|
||||
<div className="dialog-buttons">
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
color="primary"
|
||||
noForcedUpperCase
|
||||
onClick={() => handlePhoneDecision(true)}
|
||||
>
|
||||
{lang('BotAuthPhoneNumberAccept')}
|
||||
</Button>
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
color="gray"
|
||||
noForcedUpperCase
|
||||
onClick={() => handlePhoneDecision(false)}
|
||||
>
|
||||
{lang('BotAuthPhoneNumberDeny')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): Complete<StateProps> => {
|
||||
(global, { modal }): Complete<StateProps> => {
|
||||
const currentUser = selectUser(global, global.currentUserId!);
|
||||
const bot = modal?.request?.botId ? selectUser(global, modal.request.botId) : undefined;
|
||||
|
||||
return {
|
||||
bot,
|
||||
currentUser,
|
||||
};
|
||||
},
|
||||
|
||||
@ -33,7 +33,7 @@ import {
|
||||
prepareMessageReplyInfo,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
addActionHandler, getActions, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
removeBlockedUser,
|
||||
@ -1119,6 +1119,11 @@ addActionHandler('requestBotUrlAuth', async (global, actions, payload): Promise<
|
||||
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
if (result.type !== 'request') {
|
||||
handleUrlAuthResult(global, { url, result }, tabId);
|
||||
return;
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
urlAuth: {
|
||||
url,
|
||||
@ -1130,11 +1135,16 @@ addActionHandler('requestBotUrlAuth', async (global, actions, payload): Promise<
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
handleUrlAuthResult(global, actions, url, result, tabId);
|
||||
handleUrlAuthResult(global, { url, result }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('acceptBotUrlAuth', async (global, actions, payload): Promise<void> => {
|
||||
const { isWriteAllowed, tabId = getCurrentTabId() } = payload;
|
||||
const {
|
||||
isWriteAllowed,
|
||||
wasPhoneShared,
|
||||
matchCode: providedMatchCode,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.urlAuth?.button) return;
|
||||
const {
|
||||
@ -1152,10 +1162,12 @@ addActionHandler('acceptBotUrlAuth', async (global, actions, payload): Promise<v
|
||||
messageId,
|
||||
buttonId,
|
||||
isWriteAllowed,
|
||||
wasPhoneShared,
|
||||
matchCode: providedMatchCode || tabState.urlAuth.matchCode,
|
||||
});
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
handleUrlAuthResult(global, actions, url, result, tabId);
|
||||
handleUrlAuthResult(global, { url, result, wasPhoneShared }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('requestLinkUrlAuth', async (global, actions, payload): Promise<void> => {
|
||||
@ -1164,25 +1176,80 @@ addActionHandler('requestLinkUrlAuth', async (global, actions, payload): Promise
|
||||
const result = await callApi('requestLinkUrlAuth', { url });
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
if (result.type !== 'request') {
|
||||
handleUrlAuthResult(global, { url, result }, tabId);
|
||||
return;
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
urlAuth: {
|
||||
url,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
handleUrlAuthResult(global, actions, url, result, tabId);
|
||||
handleUrlAuthResult(global, { url, result }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('acceptLinkUrlAuth', async (global, actions, payload): Promise<void> => {
|
||||
const { isWriteAllowed, tabId = getCurrentTabId() } = payload;
|
||||
const {
|
||||
isWriteAllowed,
|
||||
wasPhoneShared,
|
||||
matchCode: providedMatchCode,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.urlAuth?.url) return;
|
||||
const { url } = tabState.urlAuth;
|
||||
|
||||
const result = await callApi('acceptLinkUrlAuth', { url, isWriteAllowed });
|
||||
const result = await callApi('acceptLinkUrlAuth', {
|
||||
url,
|
||||
isWriteAllowed,
|
||||
wasPhoneShared,
|
||||
matchCode: providedMatchCode || tabState.urlAuth.matchCode,
|
||||
});
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
handleUrlAuthResult(global, actions, url, result, tabId);
|
||||
handleUrlAuthResult(global, { url, result, wasPhoneShared }, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('checkUrlAuthMatchCode', async (global, actions, payload): Promise<void> => {
|
||||
const { matchCode, tabId = getCurrentTabId() } = payload;
|
||||
const url = selectTabState(global, tabId).urlAuth?.url;
|
||||
if (!url) return;
|
||||
|
||||
const { type } = await callApi('checkUrlAuthMatchCode', { url, matchCode });
|
||||
if (type === 'unmatched') {
|
||||
actions.closeUrlAuthModal({ tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'expired') {
|
||||
actions.closeUrlAuthModal({ tabId });
|
||||
actions.showNotification({ message: { key: 'ErrorUrlExpired' }, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.urlAuth) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
urlAuth: {
|
||||
...tabState.urlAuth,
|
||||
matchCode,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('declineUrlAuth', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const url = selectTabState(global, tabId).urlAuth?.url;
|
||||
if (!url) return;
|
||||
|
||||
actions.closeUrlAuthModal({ tabId });
|
||||
|
||||
await callApi('declineUrlAuth', { url });
|
||||
});
|
||||
|
||||
addActionHandler('closeUrlAuthModal', (global, actions, payload): ActionReturnType => {
|
||||
@ -1194,22 +1261,33 @@ addActionHandler('closeUrlAuthModal', (global, actions, payload): ActionReturnTy
|
||||
|
||||
function handleUrlAuthResult<T extends GlobalState>(
|
||||
global: T,
|
||||
actions: RequiredGlobalActions,
|
||||
url: string, result: ApiUrlAuthResult,
|
||||
{ url, result, wasPhoneShared }: {
|
||||
url: string;
|
||||
result: ApiUrlAuthResult;
|
||||
wasPhoneShared?: boolean;
|
||||
},
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const actions = getActions();
|
||||
if (result.type === 'expired') {
|
||||
actions.closeUrlAuthModal({ tabId });
|
||||
actions.showNotification({ message: { key: 'ErrorUrlExpired' }, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
if (result.type === 'request') {
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.urlAuth) return;
|
||||
const { domain, bot, shouldRequestWriteAccess } = result;
|
||||
global = getGlobal();
|
||||
const { type, bot, ...request } = result;
|
||||
global = updateTabState(global, {
|
||||
urlAuth: {
|
||||
...tabState.urlAuth,
|
||||
matchCode: undefined,
|
||||
request: {
|
||||
domain,
|
||||
...request,
|
||||
botId: bot.id,
|
||||
shouldRequestWriteAccess,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
@ -1218,22 +1296,41 @@ function handleUrlAuthResult<T extends GlobalState>(
|
||||
}
|
||||
|
||||
if (result.type === 'accepted' && !result.url) {
|
||||
actions.showNotification({
|
||||
message: {
|
||||
key: 'BotAuthSuccessText',
|
||||
variables: {
|
||||
url,
|
||||
if (!wasPhoneShared && tabState.urlAuth?.request?.shouldRequestPhoneNumber) {
|
||||
actions.showNotification({
|
||||
message: {
|
||||
key: 'BotAuthSuccessTextNoPhone',
|
||||
variables: {
|
||||
url: tabState.urlAuth.request?.domain || result.url,
|
||||
},
|
||||
options: {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
title: {
|
||||
key: 'BotAuthSuccessTitle',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
key: 'BotAuthSuccessTitle',
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
tabId,
|
||||
});
|
||||
} else {
|
||||
actions.showNotification({
|
||||
message: {
|
||||
key: 'BotAuthSuccessText',
|
||||
variables: {
|
||||
url: tabState.urlAuth?.request?.domain || result.url,
|
||||
},
|
||||
options: {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
},
|
||||
},
|
||||
title: {
|
||||
key: 'BotAuthSuccessTitle',
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
actions.closeUrlAuthModal({ tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3770,7 +3770,7 @@ async function openChatWithParams<T extends GlobalState>(
|
||||
if (!isTopicProcessed) {
|
||||
actions.focusMessage({
|
||||
chatId: chat.id, threadId, messageId, timestamp, tabId,
|
||||
replyMessageId: linkContext?.messageId,
|
||||
replyMessageId: linkContext?.type === 'message' ? linkContext.messageId : undefined,
|
||||
});
|
||||
}
|
||||
} else if (!isCurrentChat) {
|
||||
|
||||
@ -2308,6 +2308,8 @@ export interface ActionPayloads {
|
||||
|
||||
acceptBotUrlAuth: {
|
||||
isWriteAllowed?: boolean;
|
||||
wasPhoneShared?: boolean;
|
||||
matchCode?: string;
|
||||
} & WithTabId;
|
||||
|
||||
requestLinkUrlAuth: {
|
||||
@ -2316,8 +2318,16 @@ export interface ActionPayloads {
|
||||
|
||||
acceptLinkUrlAuth: {
|
||||
isWriteAllowed?: boolean;
|
||||
wasPhoneShared?: boolean;
|
||||
matchCode?: string;
|
||||
} & WithTabId;
|
||||
|
||||
checkUrlAuthMatchCode: {
|
||||
matchCode: string;
|
||||
} & WithTabId;
|
||||
|
||||
declineUrlAuth: WithTabId | undefined;
|
||||
|
||||
// Settings
|
||||
loadAuthorizations: undefined;
|
||||
terminateAuthorization: {
|
||||
|
||||
@ -57,6 +57,7 @@ import type {
|
||||
ApiTypePrepaidGiveaway,
|
||||
ApiTypeStoryView,
|
||||
ApiUniqueStarGiftValueInfo,
|
||||
ApiUrlAuthResultRequest,
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
} from '../../api/types';
|
||||
@ -645,11 +646,8 @@ export type TabState = {
|
||||
messageId: number;
|
||||
buttonId: number;
|
||||
};
|
||||
request?: {
|
||||
domain: string;
|
||||
botId: string;
|
||||
shouldRequestWriteAccess?: boolean;
|
||||
};
|
||||
matchCode?: string;
|
||||
request?: Omit<ApiUrlAuthResultRequest, 'type' | 'bot'> & { botId: string };
|
||||
url: string;
|
||||
};
|
||||
|
||||
|
||||
@ -1795,6 +1795,8 @@ messages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?st
|
||||
messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates;
|
||||
messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User;
|
||||
messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;
|
||||
messages.declineUrlAuth#35436bbc url:string = Bool;
|
||||
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
|
||||
@ -163,6 +163,8 @@
|
||||
"messages.getEmojiKeywordsDifference",
|
||||
"messages.requestUrlAuth",
|
||||
"messages.acceptUrlAuth",
|
||||
"messages.declineUrlAuth",
|
||||
"messages.checkUrlAuthMatchCode",
|
||||
"messages.hidePeerSettingsBar",
|
||||
"messages.getScheduledHistory",
|
||||
"messages.sendScheduledMessages",
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
--color-primary-shade: #{color.mix($color-primary, $color-black, 92%)};
|
||||
--color-primary-shade-darker: #{color.mix($color-primary, $color-black, 84%)};
|
||||
--color-primary-shade-rgb: #{toRGB(color.mix($color-primary, $color-black, 92%))};
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.15);
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.2);
|
||||
--color-primary-opacity-hover: rgba(var(--color-primary), 0.25);
|
||||
--color-primary-tint: rgba(var(--color-primary), 0.1);
|
||||
--color-green: #{$color-green};
|
||||
@ -121,7 +121,7 @@
|
||||
|
||||
--accent-color: var(--color-primary);
|
||||
--accent-background-color: var(--color-primary-tint);
|
||||
--accent-background-active-color: var(--color-primary-opacity);
|
||||
--accent-background-active-color: var(--color-primary-opacity-hover);
|
||||
|
||||
--color-error: #{$color-error};
|
||||
--color-error-shade: #{color.mix($color-error, $color-black, 92%)};
|
||||
|
||||
23
src/types/language.d.ts
vendored
23
src/types/language.d.ts
vendored
@ -548,6 +548,16 @@ export interface LangPair {
|
||||
'AboutPremiumDescription2': undefined;
|
||||
'OpenUrlTitle': undefined;
|
||||
'OpenUrlConfirm': undefined;
|
||||
'BotAuthSiteSubtitle': undefined;
|
||||
'BotAuthAllowMessages': undefined;
|
||||
'BotAuthInfo': undefined;
|
||||
'BotAuthDevice': undefined;
|
||||
'BotAuthSelectEmoji': undefined;
|
||||
'BotAuthPhoneNumber': undefined;
|
||||
'BotAuthPhoneNumberQuestion': undefined;
|
||||
'BotAuthPhoneNumberAccept': undefined;
|
||||
'BotAuthPhoneNumberDeny': undefined;
|
||||
'BotAuthLogin': undefined;
|
||||
'BotWebViewOpenBot': undefined;
|
||||
'BotChatMiniAppOpen': undefined;
|
||||
'WebAppReloadPage': undefined;
|
||||
@ -642,6 +652,7 @@ export interface LangPair {
|
||||
'ErrorPasswordChanged': undefined;
|
||||
'ErrorPasswordMissing': undefined;
|
||||
'ErrorPasskeyUnknown': undefined;
|
||||
'ErrorUrlExpired': undefined;
|
||||
'ErrorUnspecified': undefined;
|
||||
'NoStickers': undefined;
|
||||
'ClearRecentEmoji': undefined;
|
||||
@ -2181,13 +2192,16 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'OpenUrlText': {
|
||||
'url': V;
|
||||
};
|
||||
'ConversationOpenBotLinkLogin': {
|
||||
'BotAuthTitle': {
|
||||
'url': V;
|
||||
'user': V;
|
||||
};
|
||||
'ConversationOpenBotLinkAllowMessages': {
|
||||
'BotAuthAllowMessagesInfo': {
|
||||
'bot': V;
|
||||
};
|
||||
'BotAuthPhoneNumberText': {
|
||||
'domain': V;
|
||||
'phone': V;
|
||||
};
|
||||
'ForwardForStars': {
|
||||
'price': V;
|
||||
};
|
||||
@ -3575,6 +3589,9 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'BotAuthSuccessText': {
|
||||
'url': V;
|
||||
};
|
||||
'BotAuthSuccessTextNoPhone': {
|
||||
'url': V;
|
||||
};
|
||||
'RankModalMemberText': {
|
||||
'tag': V;
|
||||
'author': V;
|
||||
|
||||
@ -66,12 +66,8 @@ export const IS_EMOJI_SUPPORTED = PLATFORM_ENV && (IS_MAC_OS || IS_IOS) && isLas
|
||||
|
||||
export const IS_SERVICE_WORKER_SUPPORTED = 'serviceWorker' in navigator;
|
||||
|
||||
// Remove in mid-late 2025 when Chromium 132 is no longer a problem
|
||||
// https://issues.chromium.org/issues/390581541
|
||||
const chromeVersion = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)?.[2];
|
||||
const hasBrokenServiceWorkerStreaming = chromeVersion && Number(chromeVersion) === 132;
|
||||
// TODO Consider failed service worker
|
||||
export const IS_PROGRESSIVE_SUPPORTED = IS_SERVICE_WORKER_SUPPORTED && !hasBrokenServiceWorkerStreaming;
|
||||
export const IS_PROGRESSIVE_SUPPORTED = IS_SERVICE_WORKER_SUPPORTED;
|
||||
export const IS_OPUS_SUPPORTED = Boolean((new Audio()).canPlayType('audio/ogg; codecs=opus'));
|
||||
export const IS_CANVAS_FILTER_SUPPORTED = (
|
||||
!IS_TEST && 'filter' in (document.createElement('canvas').getContext('2d') || {})
|
||||
|
||||
@ -10,7 +10,7 @@ import { isUsernameValid } from './entities/username';
|
||||
export type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
||||
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
||||
'invoice' | 'addlist' | 'boost' | 'giftcode' | 'message' | 'premium_offer' | 'premium_multigift' | 'stars_topup'
|
||||
| 'nft' | 'stars' | 'ton' | 'stargift_auction' | 'premium';
|
||||
| 'nft' | 'stars' | 'ton' | 'stargift_auction' | 'premium' | 'oauth';
|
||||
|
||||
interface PublicMessageLink {
|
||||
type: 'publicMessageLink';
|
||||
@ -122,6 +122,11 @@ interface SettingsScreenLink {
|
||||
screen?: 'devices' | 'folders' | 'language' | 'privacy' | 'editProfile' | 'theme';
|
||||
}
|
||||
|
||||
interface OAuthLink {
|
||||
type: 'oauth';
|
||||
url: string;
|
||||
}
|
||||
|
||||
type DeepLink =
|
||||
TelegramPassportLink |
|
||||
LoginCodeLink |
|
||||
@ -139,7 +144,8 @@ type DeepLink =
|
||||
GiftAuctionLink |
|
||||
StarsModalLink |
|
||||
TonModalLink |
|
||||
SettingsScreenLink;
|
||||
SettingsScreenLink |
|
||||
OAuthLink;
|
||||
|
||||
type BuilderParams<T extends DeepLink> = Record<keyof Omit<T, 'type'>, string | undefined>;
|
||||
type BuilderReturnType<T extends DeepLink> = T | undefined;
|
||||
@ -157,6 +163,10 @@ type PublicUsernameOrBotLinkBuilderParams = Omit<BuilderParams<PublicUsernameOrB
|
||||
direct?: string;
|
||||
};
|
||||
|
||||
type OAuthLinkBuilderParams = Omit<BuilderParams<OAuthLink>, 'url'> & {
|
||||
url: string;
|
||||
};
|
||||
|
||||
const ELIGIBLE_HOSTNAMES = new Set(['t.me', 'telegram.me', 'telegram.dog']);
|
||||
|
||||
export function isDeepLink(link: string): boolean {
|
||||
@ -283,6 +293,8 @@ function parseTgLink(url: URL) {
|
||||
return { type: 'ton' } satisfies TonModalLink;
|
||||
case 'settings':
|
||||
return buildSettingsScreenLink({ screen: pathParams.length === 1 ? pathParams[0] : undefined });
|
||||
case 'oauth':
|
||||
return buildOAuthLink({ url: url.toString() });
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -455,12 +467,14 @@ function getTgDeepLinkType(
|
||||
switch (method) {
|
||||
case 'resolve': {
|
||||
const {
|
||||
|
||||
domain, post, bot_id, scope, public_key, nonce,
|
||||
domain, post, bot_id, scope, public_key, nonce, startapp,
|
||||
} = queryParams;
|
||||
if (domain === 'telegrampassport' && bot_id && scope && public_key && nonce) {
|
||||
return 'telegramPassportLink';
|
||||
}
|
||||
if (domain === 'oauth' && startapp) {
|
||||
return 'oauth';
|
||||
}
|
||||
if (domain && post) {
|
||||
return 'publicMessageLink';
|
||||
}
|
||||
@ -505,6 +519,8 @@ function getTgDeepLinkType(
|
||||
case 'settings': {
|
||||
return 'settings';
|
||||
}
|
||||
case 'oauth':
|
||||
return 'oauth';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -773,6 +789,15 @@ function buildSettingsScreenLink(params: BuilderParams<SettingsScreenLink>): Bui
|
||||
};
|
||||
}
|
||||
|
||||
function buildOAuthLink(params: OAuthLinkBuilderParams): BuilderReturnType<OAuthLink> {
|
||||
const {
|
||||
url,
|
||||
} = params;
|
||||
return {
|
||||
type: 'oauth',
|
||||
url,
|
||||
};
|
||||
}
|
||||
function buildPremiumReferrerLink(params: BuilderParams<PremiumReferrerLink>): BuilderReturnType<PremiumReferrerLink> {
|
||||
const {
|
||||
ref,
|
||||
|
||||
@ -90,30 +90,34 @@ export const processDeepLink = (url: string, linkContext?: LinkContext): boolean
|
||||
switch (parsedLink.screen) {
|
||||
case 'editProfile':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.EditProfile });
|
||||
break;
|
||||
return true;
|
||||
case 'language':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.Language });
|
||||
break;
|
||||
return true;
|
||||
case 'devices':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.ActiveSessions });
|
||||
break;
|
||||
return true;
|
||||
case 'privacy':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.Privacy });
|
||||
break;
|
||||
return true;
|
||||
case 'folders':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.Folders });
|
||||
break;
|
||||
return true;
|
||||
case 'theme':
|
||||
actions.openSettingsScreen({ screen: SettingsScreens.General });
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'stars':
|
||||
actions.openStarsBalanceModal({});
|
||||
break;
|
||||
return true;
|
||||
case 'ton':
|
||||
actions.openStarsBalanceModal({ currency: TON_CURRENCY_CODE });
|
||||
break;
|
||||
return true;
|
||||
case 'oauth':
|
||||
if (linkContext?.type !== 'inner') return false;
|
||||
actions.requestLinkUrlAuth({ url: parsedLink.url });
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user