Mini Apps: Support close confirmation (#5150)

This commit is contained in:
Alexander Zinchuk 2024-11-09 15:40:30 +04:00
parent 5e74d13044
commit dcfd14f85b
11 changed files with 170 additions and 5 deletions

View File

@ -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";

View File

@ -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';

View File

@ -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 ? <WebAppsCloseConfirmationModal isOpen={modal} /> : undefined;
};
export default WebAppsCloseConfirmationModalAsync;

View File

@ -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<OwnProps> = ({
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<HTMLDivElement>(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 (
<Modal
className={buildClassName('confirm')}
title={lang('CloseMiniApps')}
isOpen={isOpen}
onClose={onClose}
>
<p>{lang('AreYouSureCloseMiniApps')}</p>
<Checkbox
label={lang('DoNotAskAgain')}
checked={shouldSkipInFuture}
onCheck={setShouldSkipInFuture}
/>
<div
className="dialog-buttons mt-2"
ref={containerRef}
onKeyDown={handleKeyDown}
>
<Button
className="confirm-dialog-button"
isText
onClick={confirmHandler}
color="danger"
>
{oldLang('Confirm')}
</Button>
<Button className="confirm-dialog-button" isText onClick={onClose}>
{oldLang('Cancel')}
</Button>
</div>
</Modal>
);
};
export default memo(WebAppsCloseConfirmationModal);

View File

@ -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<TabState,
'starsGiftModal' |
'giftModal' |
'isGiftRecipientPickerOpen' |
'isWebAppsCloseConfirmationModalOpen' |
'giftInfoModal'
>;
@ -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[];

View File

@ -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<void> => {
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);

View File

@ -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: {},

View File

@ -246,6 +246,12 @@ export function hasOpenedWebApps<T extends GlobalState>(
return Object.keys(selectTabState(global, tabId).webApps.openedWebApps).length > 0;
}
export function hasOpenedMoreThanOneWebApps<T extends GlobalState>(
global: T, ...[tabId = getCurrentTabId()]: TabArgs<T>
): boolean {
return Object.keys(selectTabState(global, tabId).webApps.openedWebApps).length > 1;
}
export function replaceWebAppModalState<T extends GlobalState>(
global: T, modalState: WebAppModalStateType,
...[tabId = getCurrentTabId()]: TabArgs<T>

View File

@ -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;

View File

@ -131,6 +131,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
shouldCollectDebugLogs?: boolean;
shouldDebugExportedSenders?: boolean;
shouldWarnAboutSvg?: boolean;
shouldSkipWebAppCloseConfirmation: boolean;
}
export interface ApiPrivacySettings {

View File

@ -1141,6 +1141,10 @@ export interface LangPair {
'StarGiftAvailability': undefined;
'StarsSubscribeInfoLinkText': undefined;
'StarsSubscribeInfoLink': undefined;
'AreYouSureCloseMiniApps': undefined;
'CloseMiniApps': undefined;
'DoNotAskAgain': undefined;
'PaymentInfoDone': undefined;
}
export interface LangPairWithVariables<V extends unknown = LangVariable> {
@ -1530,7 +1534,6 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'StarsPerMonth': {
'amount': V;
};
'PaymentInfoDone': undefined;
}
export interface LangPairPlural {