diff --git a/src/components/modals/webApp/WebAppModalTabContent.module.scss b/src/components/modals/webApp/WebAppModalTabContent.module.scss index 155309b86..4ee358eeb 100644 --- a/src/components/modals/webApp/WebAppModalTabContent.module.scss +++ b/src/components/modals/webApp/WebAppModalTabContent.module.scss @@ -44,19 +44,19 @@ display: none; } +.secondary-button, .main-button { - position: absolute; - bottom: 0; - border-radius: 0; - - z-index: 1; + flex-grow: 1; + margin: 0.5rem; transform: translateY(100%); - transition-property: background-color, color, transform; + opacity: 0; + transition-property: background-color, color, transform, margin-inline, flex-grow, opacity; transition-duration: 0.25s; transition-timing-function: ease-in-out; &.visible { transform: translateY(0); + opacity: 1; } &.hidden { @@ -64,9 +64,84 @@ } } +.buttons-container { + display: flex; + justify-content: space-between; + position: relative; + + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ + transition-property: height; + transition-duration: 0.25s; + transition-timing-function: ease-in-out; + + height: 0rem; + + &.one-row { + align-items: center; + height: 4rem; + } + + &.two-rows { + height: 8rem; + } + + &.left-to-right { + flex-direction: row; + } + + &.right-to-left { + flex-direction: row-reverse; + } + + &.top-to-bottom, + &.bottom-to-top { + .secondary-button, + .main-button { + position: absolute; + left: 0rem; + right: 0rem; + } + } + + &.top-to-bottom { + .secondary-button { + top: 0rem; + } + .main-button { + bottom: 0rem; + } + } + &.bottom-to-top { + .main-button { + top: 0rem; + } + .secondary-button { + bottom: 0rem; + } + } +} + +.hide-horizontal { + .secondary-button, + .main-button { + transform: translateY(0); + flex-grow: 0; + margin-inline: 0; + padding-inline: 0; + width: 0; + white-space: nowrap; + overflow: hidden; + + &.visible { + margin-inline: 0.5rem; + flex-grow: 1; + } + } +} + +.secondary-button-spinner, .main-button-spinner { position: absolute; - right: 1rem; } .web-app-popup { diff --git a/src/components/modals/webApp/WebAppModalTabContent.tsx b/src/components/modals/webApp/WebAppModalTabContent.tsx index c25e5fd1d..3c7900eb1 100644 --- a/src/components/modals/webApp/WebAppModalTabContent.tsx +++ b/src/components/modals/webApp/WebAppModalTabContent.tsx @@ -23,10 +23,10 @@ import { callApi } from '../../../api/gramjs'; import { REM } from '../../common/helpers/mediaDimensions'; import renderText from '../../common/helpers/renderText'; +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useFlag from '../../../hooks/useFlag'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; -import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import useSyncEffect from '../../../hooks/useSyncEffect'; import usePopupLimit from './hooks/usePopupLimit'; import useWebAppFrame from './hooks/useWebAppFrame'; @@ -45,6 +45,7 @@ type WebAppButton = { color: string; textColor: string; isProgressVisible: boolean; + position?: 'left' | 'right' | 'top' | 'bottom'; }; export type OwnProps = { @@ -113,12 +114,14 @@ const WebAppModalTabContent: FC = ({ updateWebApp, } = getActions(); const [mainButton, setMainButton] = useState(); + const [secondaryButton, setSecondaryButton] = useState(); const [isLoaded, markLoaded, markUnloaded] = useFlag(false); const [popupParameters, setPopupParameters] = useState(); const [isRequestingPhone, setIsRequestingPhone] = useState(false); const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false); + const [bottomBarColor, setBottomBarColor] = useState(); const { unlockPopupsAt, handlePopupOpened, handlePopupClosed, } = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY); @@ -147,6 +150,7 @@ const WebAppModalTabContent: FC = ({ useEffect(() => { const themeParams = extractCurrentThemeParams(); + setBottomBarColor(themeParams.secondary_bg_color); updateCurrentWebApp({ headerColor: themeParams.bg_color, backgroundColor: themeParams.bg_color }); }, []); @@ -169,7 +173,8 @@ const WebAppModalTabContent: FC = ({ if (isActive) registerReloadFrameCallback(reloadFrame); }, [reloadFrame, registerReloadFrameCallback, isActive]); - const shouldShowMainButton = mainButton?.isVisible && mainButton.text.trim().length > 0; + const isMainButtonVisible = mainButton?.isVisible && mainButton.text.trim().length > 0; + const isSecondaryButtonVisible = secondaryButton?.isVisible && secondaryButton.text.trim().length > 0; const handleHideCloseModal = useLastCallback(() => { updateCurrentWebApp({ isCloseModalOpen: false }); @@ -191,6 +196,12 @@ const WebAppModalTabContent: FC = ({ }); }); + const handleSecondaryButtonClick = useLastCallback(() => { + sendEvent({ + eventType: 'secondary_button_pressed', + }); + }); + const handleAppPopupClose = useLastCallback((buttonId?: string) => { setPopupParameters(undefined); handlePopupClosed(); @@ -241,13 +252,6 @@ const WebAppModalTabContent: FC = ({ }, ANIMATION_WAIT); }, [theme]); - // Notify view that height changed - useSyncEffect(() => { - setTimeout(() => { - sendViewport(); - }, ANIMATION_WAIT); - }, [mainButton?.isVisible, sendViewport]); - useSyncEffect(([prevIsPaymentModalOpen]) => { if (isPaymentModalOpen === prevIsPaymentModalOpen) return; if (webApp?.slug && !isPaymentModalOpen && paymentStatus) { @@ -362,6 +366,7 @@ const WebAppModalTabContent: FC = ({ setIsRequestingPhone(false); setIsRequestingWriteAccess(false); setMainButton(undefined); + setSecondaryButton(undefined); updateCurrentWebApp({ isSettingsButtonVisible: false, shouldConfirmClosing: false, @@ -399,6 +404,10 @@ const WebAppModalTabContent: FC = ({ calculateHeaderColor(eventData.color_key, eventData.color); } + if (eventType === 'web_app_set_bottom_bar_color') { + setBottomBarColor(eventData.color); + } + if (eventType === 'web_app_data_send') { closeActiveWebApp(); sendWebViewData({ @@ -409,19 +418,32 @@ const WebAppModalTabContent: FC = ({ } if (eventType === 'web_app_setup_main_button') { - const themeParams = extractCurrentThemeParams(); - const color = validateHexColor(eventData.color) ? eventData.color : themeParams.button_color; - const textColor = validateHexColor(eventData.text_color) ? eventData.text_color : themeParams.text_color; + const color = eventData.color; + const textColor = eventData.text_color; setMainButton({ isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length), isActive: eventData.is_active, - text: eventData.text || '', + text: eventData.text, color, textColor, isProgressVisible: eventData.is_progress_visible, }); } + if (eventType === 'web_app_setup_secondary_button') { + const color = eventData.color; + const textColor = eventData.text_color; + setSecondaryButton({ + isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length), + isActive: eventData.is_active, + text: eventData.text, + color, + textColor, + isProgressVisible: eventData.is_progress_visible, + position: eventData.position, + }); + } + if (eventType === 'web_app_setup_closing_behavior') { updateCurrentWebApp({ shouldConfirmClosing: true }); } @@ -478,39 +500,99 @@ const WebAppModalTabContent: FC = ({ } } - const prevMainButtonColor = usePreviousDeprecated(mainButton?.color, true); - const prevMainButtonTextColor = usePreviousDeprecated(mainButton?.textColor, true); - const prevMainButtonIsActive = usePreviousDeprecated(mainButton && Boolean(mainButton.isActive), true); - const prevMainButtonText = usePreviousDeprecated(mainButton?.text, true); + const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true); + const mainButtonCurrentTextColor = useCurrentOrPrev(mainButton?.textColor, true); + const mainButtonCurrentIsActive = useCurrentOrPrev(mainButton && Boolean(mainButton.isActive), true); + const mainButtonCurrentText = useCurrentOrPrev(mainButton?.text, true); - const mainButtonCurrentColor = mainButton?.color || prevMainButtonColor; - const mainButtonCurrentTextColor = mainButton?.textColor || prevMainButtonTextColor; - const mainButtonCurrentIsActive = mainButton?.isActive !== undefined ? mainButton.isActive : prevMainButtonIsActive; - const mainButtonCurrentText = mainButton?.text || prevMainButtonText; + const secondaryButtonCurrentPosition = useCurrentOrPrev(secondaryButton?.position, true); + const secondaryButtonCurrentColor = useCurrentOrPrev(secondaryButton?.color, true); + const secondaryButtonCurrentTextColor = useCurrentOrPrev(secondaryButton?.textColor, true); + const secondaryButtonCurrentIsActive = useCurrentOrPrev(secondaryButton && Boolean(secondaryButton.isActive), true); + const secondaryButtonCurrentText = useCurrentOrPrev(secondaryButton?.text, true); const [shouldDecreaseWebFrameSize, setShouldDecreaseWebFrameSize] = useState(false); - const [shouldHideButton, setShouldHideButton] = useState(true); + const [shouldHideMainButton, setShouldHideMainButton] = useState(true); + const [shouldHideSecondaryButton, setShouldHideSecondaryButton] = useState(true); + const [shouldShowMainButton, setShouldShowMainButton] = useState(false); + const [shouldShowSecondaryButton, setShouldShowSecondaryButton] = useState(false); - const buttonChangeTimeout = useRef>(); + // Notify view that height changed + useSyncEffect(() => { + setTimeout(() => { + sendViewport(); + }, ANIMATION_WAIT); + }, [shouldShowSecondaryButton, shouldHideSecondaryButton, + shouldShowMainButton, shouldShowMainButton, + secondaryButton?.position, sendViewport]); + + const isVerticalLayout = secondaryButtonCurrentPosition === 'top' || secondaryButtonCurrentPosition === 'bottom'; + const isHorizontalLayout = !isVerticalLayout; + + const rowsCount = (isVerticalLayout && shouldShowMainButton && shouldShowSecondaryButton) ? 2 + : shouldShowMainButton || shouldShowSecondaryButton ? 1 : 0; + + const hideDirection = (isHorizontalLayout + && (!shouldHideMainButton && !shouldHideSecondaryButton)) ? 'horizontal' : 'vertical'; + + const mainButtonChangeTimeout = useRef>(); + const mainButtonFastTimeout = useRef>(); + const secondaryButtonChangeTimeout = useRef>(); + const secondaryButtonFastTimeout = useRef>(); useEffect(() => { - if (buttonChangeTimeout.current) clearTimeout(buttonChangeTimeout.current); - if (!shouldShowMainButton) { - setShouldDecreaseWebFrameSize(false); - buttonChangeTimeout.current = setTimeout(() => { - setShouldHideButton(true); - }, MAIN_BUTTON_ANIMATION_TIME); - } else { - setShouldHideButton(false); - buttonChangeTimeout.current = setTimeout(() => { + if (mainButtonChangeTimeout.current) clearTimeout(mainButtonChangeTimeout.current); + if (mainButtonFastTimeout.current) clearTimeout(mainButtonFastTimeout.current); + + if (isMainButtonVisible) { + mainButtonFastTimeout.current = setTimeout(() => { + setShouldShowMainButton(true); + }, 35); + setShouldHideMainButton(false); + mainButtonChangeTimeout.current = setTimeout(() => { setShouldDecreaseWebFrameSize(true); }, MAIN_BUTTON_ANIMATION_TIME); } - }, [setShouldDecreaseWebFrameSize, shouldShowMainButton]); + + if (!isMainButtonVisible) { + setShouldShowMainButton(false); + mainButtonChangeTimeout.current = setTimeout(() => { + setShouldHideMainButton(true); + }, MAIN_BUTTON_ANIMATION_TIME); + } + }, [isMainButtonVisible]); + + useEffect(() => { + if (secondaryButtonChangeTimeout.current) clearTimeout(secondaryButtonChangeTimeout.current); + if (secondaryButtonFastTimeout.current) clearTimeout(secondaryButtonFastTimeout.current); + + if (isSecondaryButtonVisible) { + secondaryButtonFastTimeout.current = setTimeout(() => { + setShouldShowSecondaryButton(true); + }, 35); + setShouldHideSecondaryButton(false); + secondaryButtonChangeTimeout.current = setTimeout(() => { + setShouldDecreaseWebFrameSize(true); + }, MAIN_BUTTON_ANIMATION_TIME); + } + + if (!isSecondaryButtonVisible) { + setShouldShowSecondaryButton(false); + secondaryButtonChangeTimeout.current = setTimeout(() => { + setShouldHideSecondaryButton(true); + }, MAIN_BUTTON_ANIMATION_TIME); + } + }, [isSecondaryButtonVisible]); + + useEffect(() => { + if (!shouldShowSecondaryButton && !shouldShowMainButton) { + setShouldDecreaseWebFrameSize(false); + } + }, [setShouldDecreaseWebFrameSize, shouldShowSecondaryButton, shouldShowMainButton]); const frameWidth = frameSize?.width || 0; let frameHeight = frameSize?.height || 0; - if (shouldDecreaseWebFrameSize) { frameHeight -= 3.5 * REM; } + if (shouldDecreaseWebFrameSize) { frameHeight -= 4 * REM; } const frameStyle = buildStyle( `left: ${0}px;`, `top: ${0}px;`, @@ -543,19 +625,53 @@ const WebAppModalTabContent: FC = ({ ref={frameRef} /> {isMaximizedState && ( - + + + ) } = D extends null ? { eventData: D; }; +export type WebAppButtonOptions = { + is_visible: boolean; + is_active: boolean; + text: string; + color: string; + text_color: string; + is_progress_visible: boolean; + position?: 'left' | 'right' | 'top' | 'bottom'; +}; + export type WebAppInboundEvent = WebAppEvent<'iframe_ready', { reload_supported?: boolean; @@ -23,14 +33,8 @@ export type WebAppInboundEvent = WebAppEvent<'web_app_data_send', { data: string; }> | - WebAppEvent<'web_app_setup_main_button', { - is_visible: boolean; - is_active: boolean; - text: string; - color: string; - text_color: string; - is_progress_visible: boolean; - }> | + WebAppEvent<'web_app_setup_main_button', WebAppButtonOptions> | + WebAppEvent<'web_app_setup_secondary_button', WebAppButtonOptions> | WebAppEvent<'web_app_setup_back_button', { is_visible: boolean; }> | @@ -52,6 +56,9 @@ export type WebAppInboundEvent = 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; }> | @@ -160,5 +167,6 @@ export type WebAppOutboundEvent = WebAppEvent<'biometry_token_updated', { status: 'updated' | 'removed' | 'failed'; }> | - WebAppEvent<'main_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed' + WebAppEvent<'main_button_pressed' | + 'secondary_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed' | 'reload_iframe', null>;