fix: prevent popup blocker from blocking async window.open (#29391)

This commit is contained in:
yyh
2025-12-10 12:46:01 +08:00
committed by GitHub
parent c033030d8c
commit f722fdfa6d
4 changed files with 115 additions and 25 deletions

View File

@@ -21,7 +21,6 @@ import {
import { useKeyPress } from 'ahooks'
import Divider from '../../base/divider'
import Loading from '../../base/loading'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
import AccessControl from '../app-access-control'
@@ -50,6 +49,7 @@ import { AppModeEnum } from '@/types/app'
import type { PublishWorkflowParams } from '@/types/workflow'
import { basePath } from '@/utils/var'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
[AccessMode.ORGANIZATION]: {
@@ -216,18 +216,23 @@ const AppPublisher = ({
setPublished(false)
}, [disabled, onToggle, open])
const handleOpenInExplore = useCallback(async () => {
try {
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
if (installed_apps?.length > 0)
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
else
const { openAsync } = useAsyncWindowOpen()
const handleOpenInExplore = useCallback(() => {
if (!appDetail?.id) return
openAsync(
async () => {
const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(appDetail.id) || {}
if (installed_apps && installed_apps.length > 0)
return `${basePath}/explore/installed/${installed_apps[0].id}`
throw new Error('No app found in Explore')
}
catch (e: any) {
Toast.notify({ type: 'error', message: `${e.message || e}` })
}
}, [appDetail?.id])
},
{
errorMessage: 'Failed to open app in Explore',
},
)
}, [appDetail?.id, openAsync])
const handleAccessControlUpdate = useCallback(async () => {
if (!appDetail)

View File

@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { type App, AppModeEnum } from '@/types/app'
import Toast, { ToastContext } from '@/app/components/base/toast'
import { ToastContext } from '@/app/components/base/toast'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import AppIcon from '@/app/components/base/app-icon'
@@ -31,6 +31,7 @@ import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { formatTime } from '@/utils/time'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import dynamic from 'next/dynamic'
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
@@ -242,20 +243,24 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
setShowAccessControl(true)
}
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
const { openAsync } = useAsyncWindowOpen()
const onClickInstalledApp = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
try {
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
if (installed_apps?.length > 0)
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
else
openAsync(
async () => {
const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(app.id) || {}
if (installed_apps && installed_apps.length > 0)
return `${basePath}/explore/installed/${installed_apps[0].id}`
throw new Error('No app found in Explore')
}
catch (e: any) {
Toast.notify({ type: 'error', message: `${e.message || e}` })
}
},
{
errorMessage: 'Failed to open app in Explore',
},
)
}
return (
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>

View File

@@ -9,6 +9,7 @@ import Toast from '../../../../base/toast'
import { PlanRange } from '../../plan-switcher/plan-range-switcher'
import { useAppContext } from '@/context/app-context'
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import List from './list'
import Button from './button'
import { Professional, Sandbox, Team } from '../../assets'
@@ -54,6 +55,8 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
})[plan]
}, [isCurrent, plan, t])
const { openAsync } = useAsyncWindowOpen()
const handleGetPayUrl = async () => {
if (loading)
return
@@ -72,8 +75,13 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
setLoading(true)
try {
if (isCurrentPaidPlan) {
const res = await fetchBillingUrl()
window.open(res.url, '_blank')
await openAsync(
() => fetchBillingUrl().then(res => res.url),
{
errorMessage: 'Failed to open billing page',
windowFeatures: 'noopener,noreferrer',
},
)
return
}

View File

@@ -0,0 +1,72 @@
import { useCallback } from 'react'
import Toast from '@/app/components/base/toast'
export type AsyncWindowOpenOptions = {
successMessage?: string
errorMessage?: string
windowFeatures?: string
onError?: (error: any) => void
onSuccess?: (url: string) => void
}
export const useAsyncWindowOpen = () => {
const openAsync = useCallback(async (
fetchUrl: () => Promise<string>,
options: AsyncWindowOpenOptions = {},
) => {
const {
successMessage,
errorMessage = 'Failed to open page',
windowFeatures = 'noopener,noreferrer',
onError,
onSuccess,
} = options
const newWindow = window.open('', '_blank', windowFeatures)
if (!newWindow) {
const error = new Error('Popup blocked by browser')
onError?.(error)
Toast.notify({
type: 'error',
message: 'Popup blocked. Please allow popups for this site.',
})
return
}
try {
const url = await fetchUrl()
if (url) {
newWindow.location.href = url
onSuccess?.(url)
if (successMessage) {
Toast.notify({
type: 'success',
message: successMessage,
})
}
}
else {
newWindow.close()
const error = new Error('Invalid URL received')
onError?.(error)
Toast.notify({
type: 'error',
message: errorMessage,
})
}
}
catch (error) {
newWindow.close()
onError?.(error)
Toast.notify({
type: 'error',
message: errorMessage,
})
}
}, [])
return { openAsync }
}