Mini Apps: Support secondary button (#5074)
This commit is contained in:
parent
e2a717dc29
commit
0eb0784165
@ -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 {
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
updateWebApp,
|
||||
} = getActions();
|
||||
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
|
||||
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>();
|
||||
|
||||
const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
|
||||
|
||||
const [popupParameters, setPopupParameters] = useState<PopupOptions | undefined>();
|
||||
const [isRequestingPhone, setIsRequestingPhone] = useState(false);
|
||||
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
|
||||
const [bottomBarColor, setBottomBarColor] = useState<string | undefined>();
|
||||
const {
|
||||
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
|
||||
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
|
||||
@ -147,6 +150,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
|
||||
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<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const handleSecondaryButtonClick = useLastCallback(() => {
|
||||
sendEvent({
|
||||
eventType: 'secondary_button_pressed',
|
||||
});
|
||||
});
|
||||
|
||||
const handleAppPopupClose = useLastCallback((buttonId?: string) => {
|
||||
setPopupParameters(undefined);
|
||||
handlePopupClosed();
|
||||
@ -241,13 +252,6 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
}, 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<OwnProps & StateProps> = ({
|
||||
setIsRequestingPhone(false);
|
||||
setIsRequestingWriteAccess(false);
|
||||
setMainButton(undefined);
|
||||
setSecondaryButton(undefined);
|
||||
updateCurrentWebApp({
|
||||
isSettingsButtonVisible: false,
|
||||
shouldConfirmClosing: false,
|
||||
@ -399,6 +404,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
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<OwnProps & StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<ReturnType<typeof setTimeout>>();
|
||||
// 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<ReturnType<typeof setTimeout>>();
|
||||
const mainButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const secondaryButtonChangeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const secondaryButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
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<OwnProps & StateProps> = ({
|
||||
ref={frameRef}
|
||||
/>
|
||||
{isMaximizedState && (
|
||||
<Button
|
||||
<div
|
||||
style={`background-color: ${bottomBarColor};`}
|
||||
className={buildClassName(
|
||||
styles.mainButton,
|
||||
shouldShowMainButton && styles.visible,
|
||||
shouldHideButton && styles.hidden,
|
||||
styles.buttonsContainer,
|
||||
secondaryButtonCurrentPosition === 'left' && styles.leftToRight,
|
||||
secondaryButtonCurrentPosition === 'right' && styles.rightToLeft,
|
||||
secondaryButtonCurrentPosition === 'top' && styles.topToBottom,
|
||||
secondaryButtonCurrentPosition === 'bottom' && styles.bottomToTop,
|
||||
hideDirection === 'horizontal' && styles.hideHorizontal,
|
||||
rowsCount === 1 && styles.oneRow,
|
||||
rowsCount === 2 && styles.twoRows,
|
||||
)}
|
||||
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
|
||||
disabled={!mainButtonCurrentIsActive}
|
||||
onClick={handleMainButtonClick}
|
||||
>
|
||||
{mainButtonCurrentText}
|
||||
{mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />}
|
||||
</Button>
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.secondaryButton,
|
||||
shouldShowSecondaryButton && !shouldHideSecondaryButton && styles.visible,
|
||||
shouldHideSecondaryButton && styles.hidden,
|
||||
)}
|
||||
fluid
|
||||
style={`background-color: ${secondaryButtonCurrentColor}; color: ${secondaryButtonCurrentTextColor}`}
|
||||
disabled={!secondaryButtonCurrentIsActive && !secondaryButton?.isProgressVisible}
|
||||
nonInteractive={secondaryButton?.isProgressVisible}
|
||||
onClick={handleSecondaryButtonClick}
|
||||
size="smaller"
|
||||
>
|
||||
{!secondaryButton?.isProgressVisible && secondaryButtonCurrentText}
|
||||
{secondaryButton?.isProgressVisible
|
||||
&& <Spinner className={styles.mainButtonSpinner} color="blue" />}
|
||||
</Button>
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.mainButton,
|
||||
shouldShowMainButton && !shouldHideMainButton && styles.visible,
|
||||
shouldHideMainButton && styles.hidden,
|
||||
)}
|
||||
fluid
|
||||
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
|
||||
disabled={!mainButtonCurrentIsActive && !mainButton?.isProgressVisible}
|
||||
nonInteractive={mainButton?.isProgressVisible}
|
||||
onClick={handleMainButtonClick}
|
||||
size="smaller"
|
||||
>
|
||||
{!mainButton?.isProgressVisible && mainButtonCurrentText}
|
||||
{mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />}
|
||||
</Button>
|
||||
</div>
|
||||
) }
|
||||
<ConfirmDialog
|
||||
isOpen={isRequestingPhone}
|
||||
|
||||
@ -16,6 +16,16 @@ type WebAppEvent<T, D> = 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>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user