From dcfd14f85b5cee6fa7d8a11e5cfae3ff54469662 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 9 Nov 2024 15:40:30 +0400 Subject: [PATCH] Mini Apps: Support close confirmation (#5150) --- src/assets/localization/fallback.strings | 3 + src/bundles/extra.ts | 1 + .../WebAppsCloseConfirmationModal.async.tsx | 16 ++++ .../main/WebAppsCloseConfirmationModal.tsx | 82 +++++++++++++++++++ src/components/modals/ModalContainer.tsx | 3 + src/global/actions/api/bots.ts | 43 +++++++++- src/global/initialState.ts | 3 + src/global/reducers/bots.ts | 6 ++ src/global/types.ts | 12 ++- src/types/index.ts | 1 + src/types/language.d.ts | 5 +- 11 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 src/components/main/WebAppsCloseConfirmationModal.async.tsx create mode 100644 src/components/main/WebAppsCloseConfirmationModal.tsx diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 153ec45be..d54464dbd 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1370,4 +1370,7 @@ "StarsSubscribeInfoLinkText" = "Terms of Service"; "StarsSubscribeInfoLink" = "https://telegram.org/tos/stars"; "StarsPerMonth" = "⭐️{amount}/month"; +"AreYouSureCloseMiniApps" = "Are you sure you want to close all Mini Apps?"; +"CloseMiniApps" = "Close Mini Apps"; +"DoNotAskAgain" = "Don't ask again"; "PaymentInfoDone" = "Proceed to checkout"; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index f0abbea3d..75274aca4 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -90,3 +90,4 @@ export { default as PaymentModal } from '../components/payment/PaymentModal'; export { default as ReceiptModal } from '../components/payment/ReceiptModal'; export { default as InviteViaLinkModal } from '../components/modals/inviteViaLink/InviteViaLinkModal'; export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal'; +export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal'; diff --git a/src/components/main/WebAppsCloseConfirmationModal.async.tsx b/src/components/main/WebAppsCloseConfirmationModal.async.tsx new file mode 100644 index 000000000..fa65b58f1 --- /dev/null +++ b/src/components/main/WebAppsCloseConfirmationModal.async.tsx @@ -0,0 +1,16 @@ +import type { FC } from '../../lib/teact/teact'; +import React from '../../lib/teact/teact'; + +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const WebAppsCloseConfirmationModalAsync: FC = (props) => { + const { modal } = props; + const WebAppsCloseConfirmationModal = useModuleLoader(Bundles.Extra, 'WebAppsCloseConfirmationModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return WebAppsCloseConfirmationModal ? : undefined; +}; + +export default WebAppsCloseConfirmationModalAsync; diff --git a/src/components/main/WebAppsCloseConfirmationModal.tsx b/src/components/main/WebAppsCloseConfirmationModal.tsx new file mode 100644 index 000000000..7c1d26039 --- /dev/null +++ b/src/components/main/WebAppsCloseConfirmationModal.tsx @@ -0,0 +1,82 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { + memo, useCallback, useRef, useState, +} from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import buildClassName from '../../util/buildClassName'; + +import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation'; +import useLang from '../../hooks/useLang'; +import useOldLang from '../../hooks/useOldLang'; + +import Button from '../ui/Button'; +import Checkbox from '../ui/Checkbox'; +import Modal from '../ui/Modal'; + +type OwnProps = { + isOpen: boolean; +}; + +const WebAppsCloseConfirmationModal: FC = ({ + isOpen, +}) => { + const oldLang = useOldLang(); + const lang = useLang(); + const { closeWebAppsCloseConfirmationModal, closeWebAppModal } = getActions(); + + const [shouldSkipInFuture, setShouldSkipInFuture] = useState(false); + + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + + const onClose = useCallback(() => { + closeWebAppsCloseConfirmationModal({ shouldSkipInFuture }); + }, [shouldSkipInFuture]); + + const confirmHandler = useCallback(() => { + closeWebAppModal({ shouldSkipConfirmation: true }); + closeWebAppsCloseConfirmationModal({ shouldSkipInFuture }); + }, [shouldSkipInFuture]); + + const handleSelectWithEnter = useCallback((index: number) => { + if (index === -1) confirmHandler(); + }, [confirmHandler]); + + const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, handleSelectWithEnter, '.Button'); + + return ( + +

{lang('AreYouSureCloseMiniApps')}

+ +
+ + +
+
+ ); +}; + +export default memo(WebAppsCloseConfirmationModal); diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 4ef21e6a5..86ec90c47 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -6,6 +6,7 @@ import type { TabState } from '../../global/types'; import { selectTabState } from '../../global/selectors'; import { pick } from '../../util/iteratees'; +import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import BoostModal from './boost/BoostModal.async'; import ChatInviteModal from './chatInvite/ChatInviteModal.async'; @@ -53,6 +54,7 @@ type ModalKey = keyof Pick; @@ -90,6 +92,7 @@ const MODALS: ModalRegistry = { starsGiftModal: StarsGiftModal, giftModal: PremiumGiftModal, isGiftRecipientPickerOpen: GiftRecipientPicker, + isWebAppsCloseConfirmationModalOpen: WebAppsCloseConfirmationModal, giftInfoModal: GiftInfoModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index 0b8983ed9..aed18b1d2 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -30,8 +30,8 @@ import { } from '../../reducers'; import { activateWebAppIfOpen, - addWebAppToOpenList, clearOpenedWebApps, hasOpenedWebApps, - removeActiveWebAppFromOpenList, removeWebAppFromOpenList, + addWebAppToOpenList, clearOpenedWebApps, hasOpenedMoreThanOneWebApps, + hasOpenedWebApps, removeActiveWebAppFromOpenList, removeWebAppFromOpenList, replaceInlineBotSettings, replaceInlineBotsIsLoading, replaceIsWebAppModalOpen, replaceWebAppModalState, updateWebApp, } from '../../reducers/bots'; @@ -704,6 +704,35 @@ addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType = } }); +addActionHandler('openWebAppsCloseConfirmationModal', (global, actions, payload): ActionReturnType => { + const { + tabId = getCurrentTabId(), + } = payload || {}; + + return updateTabState(global, { + isWebAppsCloseConfirmationModalOpen: true, + }, tabId); +}); + +addActionHandler('closeWebAppsCloseConfirmationModal', (global, actions, payload): ActionReturnType => { + const { shouldSkipInFuture, tabId = getCurrentTabId() } = payload || {}; + + global = { + ...global, + settings: { + ...global.settings, + byKey: { + ...global.settings.byKey, + shouldSkipWebAppCloseConfirmation: Boolean(shouldSkipInFuture), + }, + }, + }; + + return updateTabState(global, { + isWebAppsCloseConfirmationModalOpen: undefined, + }, tabId); +}); + addActionHandler('requestAppWebView', async (global, actions, payload): Promise => { const { botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest, @@ -869,7 +898,15 @@ addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => }); addActionHandler('closeWebAppModal', (global, actions, payload): ActionReturnType => { - const { tabId = getCurrentTabId() } = payload || {}; + const { shouldSkipConfirmation, tabId = getCurrentTabId() } = payload || {}; + + const shouldShowConfirmation = !shouldSkipConfirmation + && !global.settings.byKey.shouldSkipWebAppCloseConfirmation && hasOpenedMoreThanOneWebApps(global, tabId); + + if (shouldShowConfirmation) { + actions.openWebAppsCloseConfirmationModal({ tabId }); + return global; + } global = clearOpenedWebApps(global, tabId); if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId); diff --git a/src/global/initialState.ts b/src/global/initialState.ts index d65cb966a..d31a21fd8 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -269,6 +269,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { notificationSoundVolume: 5, shouldSuggestStickers: true, shouldSuggestCustomEmoji: true, + shouldSkipWebAppCloseConfirmation: false, shouldUpdateStickerSetOrder: true, language: 'en', timeFormat: '24h', @@ -386,6 +387,8 @@ export const INITIAL_TAB_STATE: TabState = { isShareMessageModalShown: false, + isWebAppsCloseConfirmationModalOpen: false, + forwardMessages: {}, replyingMessage: {}, diff --git a/src/global/reducers/bots.ts b/src/global/reducers/bots.ts index d9fb68ca5..c417d112b 100644 --- a/src/global/reducers/bots.ts +++ b/src/global/reducers/bots.ts @@ -246,6 +246,12 @@ export function hasOpenedWebApps( return Object.keys(selectTabState(global, tabId).webApps.openedWebApps).length > 0; } +export function hasOpenedMoreThanOneWebApps( + global: T, ...[tabId = getCurrentTabId()]: TabArgs +): boolean { + return Object.keys(selectTabState(global, tabId).webApps.openedWebApps).length > 1; +} + export function replaceWebAppModalState( global: T, modalState: WebAppModalStateType, ...[tabId = getCurrentTabId()]: TabArgs diff --git a/src/global/types.ts b/src/global/types.ts index da0024e95..4f2435d18 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -772,6 +772,8 @@ export type TabState = { onConfirm?: NoneToVoidFunction; }; + isWebAppsCloseConfirmationModalOpen?: boolean; + isGiftRecipientPickerOpen?: boolean; starsGiftingPickerModal?: { @@ -3243,7 +3245,9 @@ export interface ActionPayloads { webApp: WebApp; skipClosingConfirmation?: boolean; } & WithTabId; - closeWebAppModal: WithTabId | undefined; + closeWebAppModal: ({ + shouldSkipConfirmation?: boolean; + } & WithTabId) | undefined; changeWebAppModalState: WithTabId | undefined; // Misc @@ -3454,6 +3458,12 @@ export interface ActionPayloads { openGiftRecipientPicker: WithTabId | undefined; closeGiftRecipientPicker: WithTabId | undefined; + openWebAppsCloseConfirmationModal: WithTabId | undefined; + + closeWebAppsCloseConfirmationModal: ({ + shouldSkipInFuture?: boolean; + } & WithTabId); + openStarsGiftingPickerModal: WithTabId | undefined; closeStarsGiftingPickerModal: WithTabId | undefined; diff --git a/src/types/index.ts b/src/types/index.ts index 80ca274bd..6f8497d7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -131,6 +131,7 @@ export interface ISettings extends NotifySettings, Record { shouldCollectDebugLogs?: boolean; shouldDebugExportedSenders?: boolean; shouldWarnAboutSvg?: boolean; + shouldSkipWebAppCloseConfirmation: boolean; } export interface ApiPrivacySettings { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 8506a07d0..f21eb0a31 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1141,6 +1141,10 @@ export interface LangPair { 'StarGiftAvailability': undefined; 'StarsSubscribeInfoLinkText': undefined; 'StarsSubscribeInfoLink': undefined; + 'AreYouSureCloseMiniApps': undefined; + 'CloseMiniApps': undefined; + 'DoNotAskAgain': undefined; + 'PaymentInfoDone': undefined; } export interface LangPairWithVariables { @@ -1530,7 +1534,6 @@ export interface LangPairWithVariables { 'StarsPerMonth': { 'amount': V; }; - 'PaymentInfoDone': undefined; } export interface LangPairPlural {