Mini Apps: Support clipboard read event (#6539)

This commit is contained in:
zubiden 2025-12-23 12:50:27 +01:00 committed by Alexander Zinchuk
parent 482f9fc070
commit ef773026c0
7 changed files with 73 additions and 21 deletions

View File

@ -120,6 +120,7 @@ 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;
whitelisted_bots?: string[];
settings_display_passkeys?: boolean; settings_display_passkeys?: boolean;
passkeys_account_passkeys_max?: number; passkeys_account_passkeys_max?: number;
} }
@ -245,6 +246,7 @@ 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,
whitelistedBotIds: appConfig.whitelisted_bots,
arePasskeysAvailable: appConfig.settings_display_passkeys, arePasskeysAvailable: appConfig.settings_display_passkeys,
passkeysMaxCount: appConfig.passkeys_account_passkeys_max, passkeysMaxCount: appConfig.passkeys_account_passkeys_max,
}; };

View File

@ -285,6 +285,7 @@ export interface ApiAppConfig {
verifyAgeMin?: number; verifyAgeMin?: number;
typingDraftTtl: number; typingDraftTtl: number;
contactNoteLimit?: number; contactNoteLimit?: number;
whitelistedBotIds?: string[];
arePasskeysAvailable: boolean; arePasskeysAvailable: boolean;
passkeysMaxCount: number; passkeysMaxCount: number;
} }

View File

@ -2446,3 +2446,6 @@
"BirthdayPrivacySuggestion" = "Choose who can see your birthday in {link}"; "BirthdayPrivacySuggestion" = "Choose who can see your birthday in {link}";
"BirthdayPrivacySuggestionLink" = "Settings >"; "BirthdayPrivacySuggestionLink" = "Settings >";
"SettingsBirthday" = "Birthday"; "SettingsBirthday" = "Birthday";
"BotReadTextFromClipboardTitle" = "Clipboard Access";
"BotReadTextFromClipboardDescription" = "{bot} wants to read the contents of your clipboard. Do you want to continue?";
"BotReadTextFromClipboardConfirm" = "Allow";

View File

@ -14,7 +14,7 @@ import type {
} from '../../../types/webapp'; } from '../../../types/webapp';
import { TME_LINK_PREFIX } from '../../../config'; import { TME_LINK_PREFIX } from '../../../config';
import { convertToApiChatType } from '../../../global/helpers'; import { convertToApiChatType, getUserFullName } from '../../../global/helpers';
import { getWebAppKey } from '../../../global/helpers/bots'; import { getWebAppKey } from '../../../global/helpers/bots';
import { import {
selectBotAppPermissions, selectBotAppPermissions,
@ -144,16 +144,17 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
closeWebAppModal, closeWebAppModal,
openPreparedInlineMessageModal, openPreparedInlineMessageModal,
} = getActions(); } = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>(); const [mainButton, setMainButton] = useState<WebAppButton>();
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>(); const [secondaryButton, setSecondaryButton] = useState<WebAppButton>();
const [isLoaded, markLoaded, markUnloaded] = useFlag(false); const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
const [popupParameters, setPopupParameters] = useState<PopupOptions | undefined>(); const [popupParameters, setPopupParameters] = useState<PopupOptions>();
const [isRequestingPhone, setIsRequestingPhone] = useState(false); const [isRequestingPhone, setIsRequestingPhone] = useState(false);
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false); const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string } | undefined>(); const [clipboardRequestId, setClipboardRequestId] = useState<string>();
const [bottomBarColor, setBottomBarColor] = useState<string | undefined>(); const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string }>();
const [bottomBarColor, setBottomBarColor] = useState<string>();
const { const {
unlockPopupsAt, handlePopupOpened, handlePopupClosed, unlockPopupsAt, handlePopupOpened, handlePopupClosed,
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY); } = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
@ -218,7 +219,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
}, [themeParams]); }, [themeParams]);
const themeBackgroundColor = themeParams.bg_color; const themeBackgroundColor = themeParams.bg_color;
const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string | undefined>(); const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string>();
const backgroundColorFromSettings = theme === 'light' ? botAppSettings?.backgroundColor const backgroundColorFromSettings = theme === 'light' ? botAppSettings?.backgroundColor
: botAppSettings?.backgroundDarkColor; : botAppSettings?.backgroundDarkColor;
@ -229,7 +230,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
}, [themeBackgroundColor, backgroundColorFromEvent, backgroundColorFromSettings]); }, [themeBackgroundColor, backgroundColorFromEvent, backgroundColorFromSettings]);
const themeHeaderColor = themeParams.bg_color; const themeHeaderColor = themeParams.bg_color;
const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string | undefined>(); const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string>();
const headerColorFromSettings = theme === 'light' ? botAppSettings?.headerColor const headerColorFromSettings = theme === 'light' ? botAppSettings?.headerColor
: botAppSettings?.headerDarkColor; : botAppSettings?.headerDarkColor;
@ -473,6 +474,44 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
setIsRequestingWriteAccess(!canWrite); setIsRequestingWriteAccess(!canWrite);
} }
const handleRejectClipboardText = useLastCallback(() => {
if (!clipboardRequestId) return;
setClipboardRequestId(undefined);
sendEvent({
eventType: 'clipboard_text_received',
eventData: {
req_id: clipboardRequestId,
// eslint-disable-next-line no-null/no-null
data: null,
},
});
});
const handleConfirmClipboardText = useLastCallback(() => {
const reqId = clipboardRequestId;
if (!reqId) return;
setClipboardRequestId(undefined);
window.navigator.clipboard.readText().then((clipboardText) => {
sendEvent({
eventType: 'clipboard_text_received',
eventData: {
req_id: reqId,
data: clipboardText,
},
});
}).catch(() => {
sendEvent({
eventType: 'clipboard_text_received',
eventData: {
req_id: reqId,
// eslint-disable-next-line no-null/no-null
data: null,
},
});
});
});
async function handleCheckDownloadFile(fileUrl: string, fileName: string) { async function handleCheckDownloadFile(fileUrl: string, fileName: string) {
const canDownload = await callApi('checkBotDownloadFileParams', { const canDownload = await callApi('checkBotDownloadFileParams', {
bot: bot!, bot: bot!,
@ -531,6 +570,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
setIsRequestingWriteAccess(false); setIsRequestingWriteAccess(false);
setMainButton(undefined); setMainButton(undefined);
setSecondaryButton(undefined); setSecondaryButton(undefined);
setRequestedFileDownload(undefined);
setClipboardRequestId(undefined);
updateCurrentWebApp({ updateCurrentWebApp({
isSettingsButtonVisible: false, isSettingsButtonVisible: false,
shouldConfirmClosing: false, shouldConfirmClosing: false,
@ -766,6 +807,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
if (eventType === 'web_app_open_location_settings') { if (eventType === 'web_app_open_location_settings') {
handleOpenChat(); handleOpenChat();
} }
if (eventType === 'web_app_read_text_from_clipboard') {
setClipboardRequestId(eventData.req_id);
}
} }
const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true); const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true);
@ -1187,6 +1232,14 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
confirmHandler={handleRemoveAttachBot} confirmHandler={handleRemoveAttachBot}
confirmIsDestructive confirmIsDestructive
/> />
<ConfirmDialog
isOpen={Boolean(clipboardRequestId)}
title={lang('BotReadTextFromClipboardTitle')}
text={lang('BotReadTextFromClipboardDescription', { bot: getUserFullName(bot) })}
confirmLabel={lang('BotReadTextFromClipboardConfirm')}
onClose={handleRejectClipboardText}
confirmHandler={handleConfirmClipboardText}
/>
</div> </div>
); );
}; };

View File

@ -225,18 +225,6 @@ const useWebAppFrame = (
ignoreEventsRef.current = true; ignoreEventsRef.current = true;
} }
// Clipboard access temporarily disabled to address security concerns
if (eventType === 'web_app_read_text_from_clipboard') {
sendEvent({
eventType: 'clipboard_text_received',
eventData: {
req_id: eventData.req_id,
// eslint-disable-next-line no-null/no-null
data: null,
},
});
}
if (eventType === 'web_app_open_scan_qr_popup') { if (eventType === 'web_app_open_scan_qr_popup') {
showNotification({ showNotification({
message: 'Scanning QR code is not supported in this client yet', message: 'Scanning QR code is not supported in this client yet',

View File

@ -91,7 +91,7 @@ export function selectChatOnlineCount<T extends GlobalState>(global: T, chat: Ap
} }
export function selectIsTrustedBot<T extends GlobalState>(global: T, botId: string) { export function selectIsTrustedBot<T extends GlobalState>(global: T, botId: string) {
return global.trustedBotIds.includes(botId); return global.trustedBotIds.includes(botId) || global.appConfig.whitelistedBotIds?.includes(botId);
} }
export function selectChatType<T extends GlobalState>(global: T, chatId: string): ApiChatType | undefined { export function selectChatType<T extends GlobalState>(global: T, chatId: string): ApiChatType | undefined {

View File

@ -1821,6 +1821,8 @@ export interface LangPair {
'BirthdayRemove': undefined; 'BirthdayRemove': undefined;
'BirthdayPrivacySuggestionLink': undefined; 'BirthdayPrivacySuggestionLink': undefined;
'SettingsBirthday': undefined; 'SettingsBirthday': undefined;
'BotReadTextFromClipboardTitle': undefined;
'BotReadTextFromClipboardConfirm': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -3142,6 +3144,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'BirthdayPrivacySuggestion': { 'BirthdayPrivacySuggestion': {
'link': V; 'link': V;
}; };
'BotReadTextFromClipboardDescription': {
'bot': V;
};
} }
export interface LangPairPlural { export interface LangPairPlural {