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;
message_typing_draft_ttl?: number;
contact_note_length_limit?: number;
whitelisted_bots?: string[];
settings_display_passkeys?: boolean;
passkeys_account_passkeys_max?: number;
}
@ -245,6 +246,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
verifyAgeCountry: appConfig.verify_age_country,
verifyAgeMin: appConfig.verify_age_min,
typingDraftTtl: appConfig.message_typing_draft_ttl,
whitelistedBotIds: appConfig.whitelisted_bots,
arePasskeysAvailable: appConfig.settings_display_passkeys,
passkeysMaxCount: appConfig.passkeys_account_passkeys_max,
};

View File

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

View File

@ -2446,3 +2446,6 @@
"BirthdayPrivacySuggestion" = "Choose who can see your birthday in {link}";
"BirthdayPrivacySuggestionLink" = "Settings >";
"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';
import { TME_LINK_PREFIX } from '../../../config';
import { convertToApiChatType } from '../../../global/helpers';
import { convertToApiChatType, getUserFullName } from '../../../global/helpers';
import { getWebAppKey } from '../../../global/helpers/bots';
import {
selectBotAppPermissions,
@ -144,16 +144,17 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
closeWebAppModal,
openPreparedInlineMessageModal,
} = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>();
const [mainButton, setMainButton] = useState<WebAppButton>();
const [secondaryButton, setSecondaryButton] = useState<WebAppButton>();
const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
const [popupParameters, setPopupParameters] = useState<PopupOptions | undefined>();
const [popupParameters, setPopupParameters] = useState<PopupOptions>();
const [isRequestingPhone, setIsRequestingPhone] = useState(false);
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string } | undefined>();
const [bottomBarColor, setBottomBarColor] = useState<string | undefined>();
const [clipboardRequestId, setClipboardRequestId] = useState<string>();
const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string }>();
const [bottomBarColor, setBottomBarColor] = useState<string>();
const {
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
@ -218,7 +219,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
}, [themeParams]);
const themeBackgroundColor = themeParams.bg_color;
const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string | undefined>();
const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string>();
const backgroundColorFromSettings = theme === 'light' ? botAppSettings?.backgroundColor
: botAppSettings?.backgroundDarkColor;
@ -229,7 +230,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
}, [themeBackgroundColor, backgroundColorFromEvent, backgroundColorFromSettings]);
const themeHeaderColor = themeParams.bg_color;
const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string | undefined>();
const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string>();
const headerColorFromSettings = theme === 'light' ? botAppSettings?.headerColor
: botAppSettings?.headerDarkColor;
@ -473,6 +474,44 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
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) {
const canDownload = await callApi('checkBotDownloadFileParams', {
bot: bot!,
@ -531,6 +570,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
setIsRequestingWriteAccess(false);
setMainButton(undefined);
setSecondaryButton(undefined);
setRequestedFileDownload(undefined);
setClipboardRequestId(undefined);
updateCurrentWebApp({
isSettingsButtonVisible: false,
shouldConfirmClosing: false,
@ -766,6 +807,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
if (eventType === 'web_app_open_location_settings') {
handleOpenChat();
}
if (eventType === 'web_app_read_text_from_clipboard') {
setClipboardRequestId(eventData.req_id);
}
}
const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true);
@ -1187,6 +1232,14 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
confirmHandler={handleRemoveAttachBot}
confirmIsDestructive
/>
<ConfirmDialog
isOpen={Boolean(clipboardRequestId)}
title={lang('BotReadTextFromClipboardTitle')}
text={lang('BotReadTextFromClipboardDescription', { bot: getUserFullName(bot) })}
confirmLabel={lang('BotReadTextFromClipboardConfirm')}
onClose={handleRejectClipboardText}
confirmHandler={handleConfirmClipboardText}
/>
</div>
);
};

View File

@ -225,18 +225,6 @@ const useWebAppFrame = (
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') {
showNotification({
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) {
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 {

View File

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