fix: harden async window open placeholder logic (#29393)
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
import { useKeyPress } from 'ahooks'
|
import { useKeyPress } from 'ahooks'
|
||||||
import Divider from '../../base/divider'
|
import Divider from '../../base/divider'
|
||||||
import Loading from '../../base/loading'
|
import Loading from '../../base/loading'
|
||||||
|
import Toast from '../../base/toast'
|
||||||
import Tooltip from '../../base/tooltip'
|
import Tooltip from '../../base/tooltip'
|
||||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||||
import AccessControl from '../app-access-control'
|
import AccessControl from '../app-access-control'
|
||||||
@@ -41,6 +42,7 @@ import type { InputVar, Variable } from '@/app/components/workflow/types'
|
|||||||
import { appDefaultIconBackground } from '@/config'
|
import { appDefaultIconBackground } from '@/config'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||||
import { fetchAppDetailDirect } from '@/service/apps'
|
import { fetchAppDetailDirect } from '@/service/apps'
|
||||||
@@ -49,7 +51,6 @@ import { AppModeEnum } from '@/types/app'
|
|||||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
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 }> = {
|
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||||
[AccessMode.ORGANIZATION]: {
|
[AccessMode.ORGANIZATION]: {
|
||||||
@@ -153,6 +154,7 @@ const AppPublisher = ({
|
|||||||
|
|
||||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
||||||
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||||
|
const openAsyncWindow = useAsyncWindowOpen()
|
||||||
|
|
||||||
const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp])
|
const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp])
|
||||||
const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission])
|
const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission])
|
||||||
@@ -216,23 +218,20 @@ const AppPublisher = ({
|
|||||||
setPublished(false)
|
setPublished(false)
|
||||||
}, [disabled, onToggle, open])
|
}, [disabled, onToggle, open])
|
||||||
|
|
||||||
const { openAsync } = useAsyncWindowOpen()
|
const handleOpenInExplore = useCallback(async () => {
|
||||||
|
await openAsyncWindow(async () => {
|
||||||
const handleOpenInExplore = useCallback(() => {
|
if (!appDetail?.id)
|
||||||
if (!appDetail?.id) return
|
throw new Error('App not found')
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||||
openAsync(
|
if (installed_apps?.length > 0)
|
||||||
async () => {
|
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||||
const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(appDetail.id) || {}
|
throw new Error('No app found in Explore')
|
||||||
if (installed_apps && installed_apps.length > 0)
|
}, {
|
||||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
onError: (err) => {
|
||||||
throw new Error('No app found in Explore')
|
Toast.notify({ type: 'error', message: `${err.message || err}` })
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
errorMessage: 'Failed to open app in Explore',
|
}, [appDetail?.id, openAsyncWindow])
|
||||||
},
|
|
||||||
)
|
|
||||||
}, [appDetail?.id, openAsync])
|
|
||||||
|
|
||||||
const handleAccessControlUpdate = useCallback(async () => {
|
const handleAccessControlUpdate = useCallback(async () => {
|
||||||
if (!appDetail)
|
if (!appDetail)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
|
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { type App, AppModeEnum } from '@/types/app'
|
import { type App, AppModeEnum } from '@/types/app'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
@@ -27,11 +27,11 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
|||||||
import { fetchInstalledAppList } from '@/service/explore'
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { formatTime } from '@/utils/time'
|
import { formatTime } from '@/utils/time'
|
||||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
|
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
|
||||||
@@ -65,6 +65,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||||
const { onPlanInfoChanged } = useProviderContext()
|
const { onPlanInfoChanged } = useProviderContext()
|
||||||
const { push } = useRouter()
|
const { push } = useRouter()
|
||||||
|
const openAsyncWindow = useAsyncWindowOpen()
|
||||||
|
|
||||||
const [showEditModal, setShowEditModal] = useState(false)
|
const [showEditModal, setShowEditModal] = useState(false)
|
||||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
||||||
@@ -243,24 +244,25 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setShowAccessControl(true)
|
setShowAccessControl(true)
|
||||||
}
|
}
|
||||||
const { openAsync } = useAsyncWindowOpen()
|
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
|
||||||
const onClickInstalledApp = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
props.onClick?.()
|
props.onClick?.()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
try {
|
||||||
openAsync(
|
await openAsyncWindow(async () => {
|
||||||
async () => {
|
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||||
const { installed_apps }: { installed_apps?: { id: string }[] } = await fetchInstalledAppList(app.id) || {}
|
if (installed_apps?.length > 0)
|
||||||
if (installed_apps && installed_apps.length > 0)
|
|
||||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||||
throw new Error('No app found in Explore')
|
throw new Error('No app found in Explore')
|
||||||
},
|
}, {
|
||||||
{
|
onError: (err) => {
|
||||||
errorMessage: 'Failed to open app in Explore',
|
Toast.notify({ type: 'error', message: `${err.message || err}` })
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||||
|
|||||||
@@ -9,33 +9,28 @@ import PlanComp from '../plan'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { useBillingUrl } from '@/service/use-billing'
|
import { useBillingUrl } from '@/service/use-billing'
|
||||||
|
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||||
|
|
||||||
const Billing: FC = () => {
|
const Billing: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isCurrentWorkspaceManager } = useAppContext()
|
const { isCurrentWorkspaceManager } = useAppContext()
|
||||||
const { enableBilling } = useProviderContext()
|
const { enableBilling } = useProviderContext()
|
||||||
const { data: billingUrl, isFetching, refetch } = useBillingUrl(enableBilling && isCurrentWorkspaceManager)
|
const { data: billingUrl, isFetching, refetch } = useBillingUrl(enableBilling && isCurrentWorkspaceManager)
|
||||||
|
const openAsyncWindow = useAsyncWindowOpen()
|
||||||
|
|
||||||
const handleOpenBilling = async () => {
|
const handleOpenBilling = async () => {
|
||||||
// Open synchronously to preserve user gesture for popup blockers
|
await openAsyncWindow(async () => {
|
||||||
if (billingUrl) {
|
|
||||||
window.open(billingUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newWindow = window.open('', '_blank', 'noopener,noreferrer')
|
|
||||||
try {
|
|
||||||
const url = (await refetch()).data
|
const url = (await refetch()).data
|
||||||
if (url && newWindow) {
|
if (url)
|
||||||
newWindow.location.href = url
|
return url
|
||||||
return
|
return null
|
||||||
}
|
}, {
|
||||||
}
|
immediateUrl: billingUrl,
|
||||||
catch (err) {
|
features: 'noopener,noreferrer',
|
||||||
console.error('Failed to fetch billing url', err)
|
onError: (err) => {
|
||||||
}
|
console.error('Failed to fetch billing url', err)
|
||||||
// Close the placeholder window if we failed to fetch the URL
|
},
|
||||||
newWindow?.close()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||||||
const isCurrentPaidPlan = isCurrent && !isFreePlan
|
const isCurrentPaidPlan = isCurrent && !isFreePlan
|
||||||
const isPlanDisabled = isCurrentPaidPlan ? false : planInfo.level <= ALL_PLANS[currentPlan].level
|
const isPlanDisabled = isCurrentPaidPlan ? false : planInfo.level <= ALL_PLANS[currentPlan].level
|
||||||
const { isCurrentWorkspaceManager } = useAppContext()
|
const { isCurrentWorkspaceManager } = useAppContext()
|
||||||
|
const openAsyncWindow = useAsyncWindowOpen()
|
||||||
|
|
||||||
const btnText = useMemo(() => {
|
const btnText = useMemo(() => {
|
||||||
if (isCurrent)
|
if (isCurrent)
|
||||||
@@ -55,8 +56,6 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||||||
})[plan]
|
})[plan]
|
||||||
}, [isCurrent, plan, t])
|
}, [isCurrent, plan, t])
|
||||||
|
|
||||||
const { openAsync } = useAsyncWindowOpen()
|
|
||||||
|
|
||||||
const handleGetPayUrl = async () => {
|
const handleGetPayUrl = async () => {
|
||||||
if (loading)
|
if (loading)
|
||||||
return
|
return
|
||||||
@@ -75,13 +74,16 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (isCurrentPaidPlan) {
|
if (isCurrentPaidPlan) {
|
||||||
await openAsync(
|
await openAsyncWindow(async () => {
|
||||||
() => fetchBillingUrl().then(res => res.url),
|
const res = await fetchBillingUrl()
|
||||||
{
|
if (res.url)
|
||||||
errorMessage: 'Failed to open billing page',
|
return res.url
|
||||||
windowFeatures: 'noopener,noreferrer',
|
throw new Error('Failed to open billing page')
|
||||||
|
}, {
|
||||||
|
onError: (err) => {
|
||||||
|
Toast.notify({ type: 'error', message: err.message || String(err) })
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
web/hooks/use-async-window-open.spec.ts
Normal file
116
web/hooks/use-async-window-open.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import { useAsyncWindowOpen } from './use-async-window-open'
|
||||||
|
|
||||||
|
describe('useAsyncWindowOpen', () => {
|
||||||
|
const originalOpen = window.open
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
window.open = originalOpen
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens immediate url synchronously without calling async getter', async () => {
|
||||||
|
const openSpy = jest.fn()
|
||||||
|
window.open = openSpy
|
||||||
|
const getUrl = jest.fn()
|
||||||
|
const { result } = renderHook(() => useAsyncWindowOpen())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current(getUrl, {
|
||||||
|
immediateUrl: 'https://example.com',
|
||||||
|
target: '_blank',
|
||||||
|
features: 'noopener,noreferrer',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(openSpy).toHaveBeenCalledWith('https://example.com', '_blank', 'noopener,noreferrer')
|
||||||
|
expect(getUrl).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets opener to null and redirects when async url resolves', async () => {
|
||||||
|
const close = jest.fn()
|
||||||
|
const mockWindow: any = {
|
||||||
|
location: { href: '' },
|
||||||
|
close,
|
||||||
|
opener: 'should-be-cleared',
|
||||||
|
}
|
||||||
|
const openSpy = jest.fn(() => mockWindow)
|
||||||
|
window.open = openSpy
|
||||||
|
const { result } = renderHook(() => useAsyncWindowOpen())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current(async () => 'https://example.com/path')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(openSpy).toHaveBeenCalledWith('about:blank', '_blank', undefined)
|
||||||
|
expect(mockWindow.opener).toBeNull()
|
||||||
|
expect(mockWindow.location.href).toBe('https://example.com/path')
|
||||||
|
expect(close).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes placeholder and forwards error when async getter throws', async () => {
|
||||||
|
const close = jest.fn()
|
||||||
|
const mockWindow: any = {
|
||||||
|
location: { href: '' },
|
||||||
|
close,
|
||||||
|
opener: null,
|
||||||
|
}
|
||||||
|
const openSpy = jest.fn(() => mockWindow)
|
||||||
|
window.open = openSpy
|
||||||
|
const onError = jest.fn()
|
||||||
|
const { result } = renderHook(() => useAsyncWindowOpen())
|
||||||
|
|
||||||
|
const error = new Error('fetch failed')
|
||||||
|
await act(async () => {
|
||||||
|
await result.current(async () => {
|
||||||
|
throw error
|
||||||
|
}, { onError })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(close).toHaveBeenCalled()
|
||||||
|
expect(onError).toHaveBeenCalledWith(error)
|
||||||
|
expect(mockWindow.location.href).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes placeholder and reports when no url is returned', async () => {
|
||||||
|
const close = jest.fn()
|
||||||
|
const mockWindow: any = {
|
||||||
|
location: { href: '' },
|
||||||
|
close,
|
||||||
|
opener: null,
|
||||||
|
}
|
||||||
|
const openSpy = jest.fn(() => mockWindow)
|
||||||
|
window.open = openSpy
|
||||||
|
const onError = jest.fn()
|
||||||
|
const { result } = renderHook(() => useAsyncWindowOpen())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current(async () => null, { onError })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(close).toHaveBeenCalled()
|
||||||
|
expect(onError).toHaveBeenCalled()
|
||||||
|
const errArg = onError.mock.calls[0][0] as Error
|
||||||
|
expect(errArg.message).toBe('No url resolved for new window')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports failure when window.open returns null', async () => {
|
||||||
|
const openSpy = jest.fn(() => null)
|
||||||
|
window.open = openSpy
|
||||||
|
const getUrl = jest.fn()
|
||||||
|
const onError = jest.fn()
|
||||||
|
const { result } = renderHook(() => useAsyncWindowOpen())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current(getUrl, { onError })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalled()
|
||||||
|
const errArg = onError.mock.calls[0][0] as Error
|
||||||
|
expect(errArg.message).toBe('Failed to open new window')
|
||||||
|
expect(getUrl).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,72 +1,49 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import Toast from '@/app/components/base/toast'
|
|
||||||
|
|
||||||
export type AsyncWindowOpenOptions = {
|
type GetUrl = () => Promise<string | null | undefined>
|
||||||
successMessage?: string
|
|
||||||
errorMessage?: string
|
type AsyncWindowOpenOptions = {
|
||||||
windowFeatures?: string
|
immediateUrl?: string | null
|
||||||
onError?: (error: any) => void
|
target?: string
|
||||||
onSuccess?: (url: string) => void
|
features?: string
|
||||||
|
onError?: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAsyncWindowOpen = () => {
|
export const useAsyncWindowOpen = () => useCallback(async (getUrl: GetUrl, options?: AsyncWindowOpenOptions) => {
|
||||||
const openAsync = useCallback(async (
|
const {
|
||||||
fetchUrl: () => Promise<string>,
|
immediateUrl,
|
||||||
options: AsyncWindowOpenOptions = {},
|
target = '_blank',
|
||||||
) => {
|
features,
|
||||||
const {
|
onError,
|
||||||
successMessage,
|
} = options ?? {}
|
||||||
errorMessage = 'Failed to open page',
|
|
||||||
windowFeatures = 'noopener,noreferrer',
|
|
||||||
onError,
|
|
||||||
onSuccess,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const newWindow = window.open('', '_blank', windowFeatures)
|
if (immediateUrl) {
|
||||||
|
window.open(immediateUrl, target, features)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!newWindow) {
|
const newWindow = window.open('about:blank', target, features)
|
||||||
const error = new Error('Popup blocked by browser')
|
if (!newWindow) {
|
||||||
onError?.(error)
|
onError?.(new Error('Failed to open new window'))
|
||||||
Toast.notify({
|
return
|
||||||
type: 'error',
|
}
|
||||||
message: 'Popup blocked. Please allow popups for this site.',
|
|
||||||
})
|
try {
|
||||||
|
newWindow.opener = null
|
||||||
|
}
|
||||||
|
catch { /* noop */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = await getUrl()
|
||||||
|
if (url) {
|
||||||
|
newWindow.location.href = url
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
newWindow.close()
|
||||||
try {
|
onError?.(new Error('No url resolved for new window'))
|
||||||
const url = await fetchUrl()
|
}
|
||||||
|
catch (error) {
|
||||||
if (url) {
|
newWindow.close()
|
||||||
newWindow.location.href = url
|
onError?.(error instanceof Error ? error : new Error(String(error)))
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user