From 2bbfc15dd5cd6768b4d5e596211384fd8ea92b0c Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Wed, 23 Apr 2025 18:59:15 +0200 Subject: [PATCH] Mini App: Correctly respond for unsupported APIs (#5848) --- .../modals/webApp/WebAppModalTabContent.tsx | 2 +- .../modals/webApp/hooks/useWebAppFrame.ts | 72 ++++ src/types/webapp.ts | 374 ++++++++++-------- 3 files changed, 292 insertions(+), 156 deletions(-) diff --git a/src/components/modals/webApp/WebAppModalTabContent.tsx b/src/components/modals/webApp/WebAppModalTabContent.tsx index 8ffc6be27..a94d26b54 100644 --- a/src/components/modals/webApp/WebAppModalTabContent.tsx +++ b/src/components/modals/webApp/WebAppModalTabContent.tsx @@ -1060,7 +1060,7 @@ const WebAppModalTabContent: FC = ({ src={url} title={`${bot?.firstName} Web App`} sandbox={SANDBOX_ATTRIBUTES} - allow="camera; microphone; geolocation;" + allow="camera; microphone; geolocation; clipboard-write;" allowFullScreen ref={frameRef} /> diff --git a/src/components/modals/webApp/hooks/useWebAppFrame.ts b/src/components/modals/webApp/hooks/useWebAppFrame.ts index ed4304397..7a22821c9 100644 --- a/src/components/modals/webApp/hooks/useWebAppFrame.ts +++ b/src/components/modals/webApp/hooks/useWebAppFrame.ts @@ -265,6 +265,78 @@ const useWebAppFrame = ( }); } + if (eventType === 'web_app_device_storage_clear' + || eventType === 'web_app_device_storage_get_key' + || eventType === 'web_app_device_storage_save_key') { + const { req_id } = eventData; + sendEvent({ + eventType: 'device_storage_failed', + eventData: { + req_id, + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_secure_storage_clear' + || eventType === 'web_app_secure_storage_get_key' + || eventType === 'web_app_secure_storage_restore_key' + || eventType === 'web_app_secure_storage_save_key') { + const { req_id } = eventData; + sendEvent({ + eventType: 'secure_storage_failed', + eventData: { + req_id, + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_start_accelerometer') { + sendEvent({ + eventType: 'accelerometer_failed', + eventData: { + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_start_gyroscope') { + sendEvent({ + eventType: 'gyroscope_failed', + eventData: { + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_start_device_orientation') { + sendEvent({ + eventType: 'device_orientation_failed', + eventData: { + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_add_to_home_screen') { + sendEvent({ + eventType: 'home_screen_failed', + eventData: { + error: 'UNSUPPORTED', + }, + }); + } + + if (eventType === 'web_app_check_home_screen') { + sendEvent({ + eventType: 'home_screen_checked', + eventData: { + status: 'unsupported', + }, + }); + } + if (eventType === 'web_app_set_emoji_status') { const { custom_emoji_id, duration } = eventData; diff --git a/src/types/webapp.ts b/src/types/webapp.ts index 7f745387e..6aae09fab 100644 --- a/src/types/webapp.ts +++ b/src/types/webapp.ts @@ -60,105 +60,115 @@ export type SafeArea = { right: number; }; -export type WebAppInboundEvent = - WebAppEvent<'iframe_ready', { - reload_supported?: boolean; - }> | - WebAppEvent<'web_app_data_send', { - data: string; - }> | - WebAppEvent<'web_app_setup_main_button', WebAppButtonOptions> | - WebAppEvent<'web_app_setup_secondary_button', WebAppButtonOptions> | - WebAppEvent<'web_app_setup_back_button', { - is_visible: boolean; - }> | - WebAppEvent<'web_app_setup_settings_button', { - is_visible: boolean; - }> | - WebAppEvent<'web_app_open_link', { - url: string; - try_instant_view?: boolean; - }> | - WebAppEvent<'web_app_open_tg_link', { - path_full: string; - force_request?: boolean; - }> | - WebAppEvent<'web_app_open_invoice', { - slug: string; - }> | - WebAppEvent<'web_app_trigger_haptic_feedback', { +interface WebAppInboundEventMap { + iframe_ready: { reload_supported?: boolean }; + web_app_data_send: { data: string }; + web_app_setup_main_button: WebAppButtonOptions; + web_app_setup_secondary_button: WebAppButtonOptions; + web_app_setup_back_button: { is_visible: boolean }; + web_app_setup_settings_button: { is_visible: boolean }; + web_app_open_link: { url: string; try_instant_view?: boolean }; + web_app_open_tg_link: { path_full: string; force_request?: boolean }; + web_app_open_invoice: { slug: string }; + web_app_trigger_haptic_feedback: { type: 'impact' | 'notification' | 'selection_change'; impact_style?: 'light' | 'medium' | 'heavy'; notification_type?: 'error' | 'success' | 'warning'; - }> | - WebAppEvent<'web_app_set_bottom_bar_color', { - color: string; - }> | - WebAppEvent<'web_app_set_background_color', { - color: string; - }> | - WebAppEvent<'web_app_set_header_color', { - color_key?: 'bg_color' | 'secondary_bg_color'; - color?: string; - }> | - WebAppEvent<'web_app_open_popup', PopupOptions> | - WebAppEvent<'web_app_setup_closing_behavior', { - need_confirmation: boolean; - }> | - WebAppEvent<'web_app_open_scan_qr_popup', { - text?: string; - }> | - WebAppEvent<'web_app_read_text_from_clipboard', { - req_id: string; - }> | - WebAppEvent<'web_app_switch_inline_query', { + }; + web_app_set_bottom_bar_color: { color: string }; + web_app_set_background_color: { color: string }; + web_app_set_header_color: { color_key?: 'bg_color' | 'secondary_bg_color'; color?: string }; + web_app_open_popup: PopupOptions; + web_app_setup_closing_behavior: { need_confirmation: boolean }; + web_app_open_scan_qr_popup: { text?: string }; + web_app_read_text_from_clipboard: { req_id: string }; + web_app_switch_inline_query: { query: string; chat_types: ('users' | 'bots' | 'groups' | 'channels')[]; - }> | - WebAppEvent<'web_app_invoke_custom_method', { + }; + web_app_invoke_custom_method: { req_id: string; method: string; params: object }; + web_app_biometry_request_access: { reason: string }; + web_app_biometry_request_auth: { reason: string }; + web_app_biometry_update_token: { token: string }; + web_app_set_emoji_status: { custom_emoji_id: string; duration?: number }; + web_app_request_file_download: { url: string; file_name: string }; + web_app_send_prepared_message: { id: string }; + web_app_device_storage_save_key: { req_id: string; - method: string; - params: object; - }> | - WebAppEvent<'web_app_biometry_request_access', { - reason: string; - }> | - WebAppEvent<'web_app_biometry_request_auth', { - reason: string; - }> | - WebAppEvent<'web_app_biometry_update_token', { - token: string; - }> | - WebAppEvent<'web_app_set_emoji_status', { - custom_emoji_id: string; - duration?: number; - }> | - WebAppEvent<'web_app_request_file_download', { - url: string; - file_name: string; - }> | - WebAppEvent<'web_app_send_prepared_message', { - id: string; - }> | - WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand' - | 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup' - | 'web_app_request_write_access' | 'iframe_will_reload' - | 'web_app_biometry_get_info' | 'web_app_biometry_open_settings' | 'web_app_request_emoji_status_access' - | 'web_app_check_location' | 'web_app_request_location' | 'web_app_open_location_settings' - | 'web_app_request_fullscreen' | 'web_app_exit_fullscreen' - | 'web_app_request_safe_area' | 'web_app_request_content_safe_area', - null>; + key: string; + value: unknown | null; + }; + web_app_device_storage_get_key: { + req_id: string; + key: string; + }; + web_app_device_storage_clear: { + req_id: string; + }; + web_app_secure_storage_save_key: { + req_id: string; + key: string; + value: unknown | null; + }; + web_app_secure_storage_get_key: { + req_id: string; + key: string; + }; + web_app_secure_storage_restore_key: { + req_id: string; + key: string; + }; + web_app_secure_storage_clear: { + req_id: string; + }; + web_app_start_accelerometer: { + refresh_rate?: number; + }; + web_app_start_gyroscope: { + refresh_rate?: number; + }; + web_app_start_device_orientation: { + refresh_rate?: number; + need_absolute?: boolean; + }; -export type WebAppOutboundEvent = - WebAppEvent<'viewport_changed', { + // No payload + web_app_request_viewport: null; + web_app_request_theme: null; + web_app_ready: null; + web_app_expand: null; + web_app_request_phone: null; + web_app_close: null; + web_app_close_scan_qr_popup: null; + web_app_request_write_access: null; + iframe_will_reload: null; + web_app_biometry_get_info: null; + web_app_biometry_open_settings: null; + web_app_request_emoji_status_access: null; + web_app_check_location: null; + web_app_request_location: null; + web_app_open_location_settings: null; + web_app_request_fullscreen: null; + web_app_exit_fullscreen: null; + web_app_request_safe_area: null; + web_app_request_content_safe_area: null; + web_app_stop_accelerometer: null; + web_app_stop_gyroscope: null; + web_app_stop_device_orientation: null; + web_app_add_to_home_screen: null; + web_app_check_home_screen: null; +} + +interface WebAppOutboundEventMap { + viewport_changed: { height: number; width?: number; is_expanded?: boolean; is_state_stable?: boolean; - }> | - WebAppEvent<'content_safe_area_changed', SafeArea> | - WebAppEvent<'safe_area_changed', SafeArea> | - WebAppEvent<'theme_changed', { + }; + content_safe_area_changed: SafeArea; + safe_area_changed: SafeArea; + theme_changed: { theme_params: { bg_color: string; text_color: string; @@ -168,76 +178,69 @@ export type WebAppOutboundEvent = button_text_color: string; secondary_bg_color: string; }; - }> | - WebAppEvent<'set_custom_style', string> | - WebAppEvent<'invoice_closed', { + }; + set_custom_style: string; + invoice_closed: { slug: string; status: 'paid' | 'cancelled' | 'pending' | 'failed'; - }> | - WebAppEvent<'phone_requested', { - phone_number: string; - }> | - WebAppEvent<'popup_closed', { + }; + phone_requested: { + status: 'sent' | 'cancelled'; + }; + popup_closed: { button_id?: string; - }> | - WebAppEvent<'fullscreen_changed', { + }; + fullscreen_changed: { is_fullscreen: boolean; - }> | - WebAppEvent<'visibility_changed', { + }; + visibility_changed: { is_visible: boolean; - }> | - WebAppEvent<'fullscreen_failed', { + }; + fullscreen_failed: { error: 'UNSUPPORTED' | string; - }> | - WebAppEvent<'qr_text_received', { + }; + qr_text_received: { data: string; - }> | - WebAppEvent<'clipboard_text_received', { + }; + clipboard_text_received: { req_id: string; data: string | null; - }> | - WebAppEvent<'write_access_requested', { + }; + write_access_requested: { status: 'allowed' | 'cancelled'; - }> | - WebAppEvent<'phone_requested', { - status: 'sent' | 'cancelled'; - }> | - WebAppEvent<'custom_method_invoked', { + }; + custom_method_invoked: { req_id: string; - } & ({ - result: object; - } | { - error: string; - })> | - WebAppEvent<'biometry_info_received', { - available: false; - } | { + } & ( + { result: object } | + { error: string } + ); + biometry_info_received: + | { available: false } + | { available: true; type: 'finger' | 'face' | 'unknown'; access_requested: boolean; access_granted: boolean; token_saved: boolean; device_id: string; - }> | - WebAppEvent<'biometry_auth_requested', { - status: 'authorized'; - token: string; - } | { - status: 'failed'; - }> | - WebAppEvent<'biometry_token_updated', { + }; + biometry_auth_requested: + | { status: 'authorized'; token: string } + | { status: 'failed' }; + biometry_token_updated: { status: 'updated' | 'removed' | 'failed'; - }> | - WebAppEvent<'location_checked', { - available: false; - } | { + }; + location_checked: + | { available: false } + | { available: boolean; access_requested: boolean; access_granted?: boolean; - }> | - WebAppEvent<'location_requested', { - available: boolean; - } | { + }; + location_requested: + | { available: boolean } + | { available: boolean; latitude: number; longitude: number; @@ -248,24 +251,85 @@ export type WebAppOutboundEvent = vertical_accuracy: number | null; course_accuracy: number | null; speed_accuracy: number | null; - }> | - WebAppEvent<'emoji_status_access_requested', { + }; + emoji_status_access_requested: { status: 'allowed' | 'cancelled'; - }> | - WebAppEvent<'access_requested', { + }; + access_requested: { available: true; - }> | - WebAppEvent<'emoji_status_failed', { - error: 'UNSUPPORTED' | 'USER_DECLINED' | 'SUGGESTED_EMOJI_INVALID' - | 'DURATION_INVALID' | 'SERVER_ERROR' | 'UNKNOWN_ERROR'; - }> | - WebAppEvent<'file_download_requested', { + }; + emoji_status_failed: { + error: + | 'UNSUPPORTED' + | 'USER_DECLINED' + | 'SUGGESTED_EMOJI_INVALID' + | 'DURATION_INVALID' + | 'SERVER_ERROR' + | 'UNKNOWN_ERROR'; + }; + file_download_requested: { status: 'cancelled' | 'downloading'; - }> | - WebAppEvent<'prepared_message_failed', { - error: 'UNSUPPORTED' | 'MESSAGE_EXPIRED' | 'MESSAGE_SEND_FAILED' - | 'USER_DECLINED' | 'UNKNOWN_ERROR'; - }> | - WebAppEvent<'main_button_pressed' | - 'secondary_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed' - | 'reload_iframe' | 'prepared_message_sent' | 'emoji_status_set', null>; + }; + prepared_message_failed: { + error: + | 'UNSUPPORTED' + | 'MESSAGE_EXPIRED' + | 'MESSAGE_SEND_FAILED' + | 'USER_DECLINED' + | 'UNKNOWN_ERROR'; + }; + device_storage_failed: { + req_id: string; + error: + | 'UNSUPPORTED' + | 'KEY_INVALID' + | 'VALUE_INVALID' + | 'QUOTA_EXCEEDED' + | 'UNKNOWN_ERROR'; + }; + secure_storage_failed: { + req_id: string; + error: + | 'UNSUPPORTED' + | 'KEY_INVALID' + | 'VALUE_INVALID' + | 'QUOTA_EXCEEDED' + | 'STORAGE_NOT_EMPTY' + | 'RESTORE_UNAVAILABLE' + | 'RESTORE_CANCELLED' + | 'UNKNOWN_ERROR'; + }; + accelerometer_failed: { + error: 'UNSUPPORTED'; + }; + gyroscope_failed: { + error: 'UNSUPPORTED'; + }; + device_orientation_failed: { + error: 'UNSUPPORTED'; + }; + home_screen_failed: { + error: 'UNSUPPORTED'; + }; + home_screen_checked: { + status: 'unsupported' | 'unknown' | 'added' | 'missed'; + }; + main_button_pressed: null; + secondary_button_pressed: null; + back_button_pressed: null; + settings_button_pressed: null; + scan_qr_popup_closed: null; + reload_iframe: null; + prepared_message_sent: null; + emoji_status_set: null; +} + +export type WebAppInboundEvent = +{ [K in keyof WebAppInboundEventMap]: + WebAppEvent +}[keyof WebAppInboundEventMap]; + +export type WebAppOutboundEvent = +{ [K in keyof WebAppOutboundEventMap]: + WebAppEvent +}[keyof WebAppOutboundEventMap];