diff --git a/packages/frontend/core/src/commands/affine-help.tsx b/packages/frontend/core/src/commands/affine-help.tsx index 1d1d77ffe0..130f86b306 100644 --- a/packages/frontend/core/src/commands/affine-help.tsx +++ b/packages/frontend/core/src/commands/affine-help.tsx @@ -4,6 +4,7 @@ import { registerAffineCommand } from '@toeverything/infra'; import type { createStore } from 'jotai'; import { openSettingModalAtom } from '../atoms'; +import { popupWindow } from '../utils'; export function registerAffineHelpCommands({ t, @@ -20,7 +21,7 @@ export function registerAffineHelpCommands({ icon: , label: t['com.affine.cmdk.affine.whats-new'](), run() { - window.open(runtimeConfig.changelogUrl, '_blank'); + popupWindow(runtimeConfig.changelogUrl); }, }) ); diff --git a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx index d66f17bfbd..aa1ae14b61 100644 --- a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx +++ b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx @@ -4,6 +4,7 @@ import { Loading } from '@affine/component/ui/loading'; import { AffineShapeIcon } from '@affine/core/components/page-list'; import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { popupWindow } from '@affine/core/utils'; import { SubscriptionPlan, type SubscriptionRecurring } from '@affine/graphql'; import { changePasswordMutation, @@ -48,7 +49,7 @@ const usePaymentRedirect = () => { successCallbackLink: null, }, }); - window.open(checkoutUrl, '_self', 'norefferer'); + popupWindow(checkoutUrl); }, [recurring, plan, coupon, idempotencyKey, checkoutSubscription]); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx index ae50fcee16..b5fcca1655 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/about/index.tsx @@ -11,7 +11,7 @@ import { useCallback } from 'react'; import { useAppSettingHelper } from '../../../../../hooks/affine/use-app-setting-helper'; import { appIconMap, appNames } from '../../../../../pages/open-app'; -import { mixpanel } from '../../../../../utils'; +import { mixpanel, popupWindow } from '../../../../../utils'; import { relatedLinks } from './config'; import * as styles from './style.css'; import { UpdateCheckSection } from './update-check-section'; @@ -99,7 +99,7 @@ export const AboutAffine = () => { desc={t['com.affine.aboutAFFiNE.changelog.description']()} style={{ cursor: 'pointer' }} onClick={() => { - window.open(runtimeConfig.changelogUrl, '_blank'); + popupWindow(runtimeConfig.changelogUrl); }} > @@ -143,7 +143,7 @@ export const AboutAffine = () => {
{ - window.open(link, '_blank'); + popupWindow(link); }} key={title} > diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index 761e107a8b..c9a8c0089a 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -31,7 +31,7 @@ import { useMutation } from '../../../../../hooks/use-mutation'; import { useQuery } from '../../../../../hooks/use-query'; import type { SubscriptionMutator } from '../../../../../hooks/use-subscription'; import { useUserSubscription } from '../../../../../hooks/use-subscription'; -import { mixpanel } from '../../../../../utils'; +import { mixpanel, popupWindow } from '../../../../../utils'; import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary'; import { CancelAction, ResumeAction } from '../plans/actions'; import * as styles from './style.css'; @@ -262,7 +262,7 @@ const PaymentMethodUpdater = () => { const update = useAsyncCallback(async () => { await trigger(null, { onSuccess: data => { - window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer'); + popupWindow(data.createCustomerPortal); }, }); }, [trigger]); @@ -361,7 +361,7 @@ const InvoiceLine = ({ const open = useCallback(() => { if (invoice.link) { - window.open(invoice.link, '_blank', 'noopener noreferrer'); + popupWindow(invoice.link); } }, [invoice.link]); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx index cdad60b361..f29abbb649 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/ai/subscribe.tsx @@ -1,6 +1,7 @@ import { Button } from '@affine/component'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useMutation } from '@affine/core/hooks/use-mutation'; +import { popupWindow } from '@affine/core/utils'; import { createCheckoutSessionMutation } from '@affine/graphql'; import { nanoid } from 'nanoid'; import { useCallback, useEffect, useMemo, useRef } from 'react'; @@ -51,11 +52,7 @@ export const AISubscribe = ({ }, { onSuccess: data => { - const newTab = window.open( - data.createCheckoutSession, - '_blank', - 'noopener noreferrer' - ); + const newTab = popupWindow(data.createCheckoutSession); if (newTab) { newTabRef.current = newTab; newTab.addEventListener('close', onClose); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx index 469a88c855..2199e387d5 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx @@ -5,6 +5,7 @@ import type { Subscription, SubscriptionMutator, } from '@affine/core/hooks/use-subscription'; +import { popupWindow } from '@affine/core/utils'; import type { SubscriptionRecurring } from '@affine/graphql'; import { createCheckoutSessionMutation, @@ -309,13 +310,7 @@ const Upgrade = ({ }, { onSuccess: data => { - // FIXME: safari prevents from opening new tab by window api - // TODO(@xp): what if electron? - const newTab = window.open( - data.createCheckoutSession, - '_blank', - 'noopener noreferrer' - ); + const newTab = popupWindow(data.createCheckoutSession); if (newTab) { newTabRef.current = newTab; diff --git a/packages/frontend/core/src/components/app-sidebar/app-updater-button/index.tsx b/packages/frontend/core/src/components/app-sidebar/app-updater-button/index.tsx index aeee9fc946..08a5c85f42 100644 --- a/packages/frontend/core/src/components/app-sidebar/app-updater-button/index.tsx +++ b/packages/frontend/core/src/components/app-sidebar/app-updater-button/index.tsx @@ -1,4 +1,5 @@ import { Tooltip } from '@affine/component'; +import { popupWindow } from '@affine/core/utils'; import { Unreachable } from '@affine/env/constant'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons'; @@ -181,13 +182,12 @@ export function AppUpdaterButton({ onDownloadUpdate(); } } else { - window.open( - `https://github.com/toeverything/AFFiNE/releases/tag/v${updateAvailable.version}`, - '_blank' + popupWindow( + `https://github.com/toeverything/AFFiNE/releases/tag/v${updateAvailable.version}` ); } } else if (changelogUnread) { - window.open(runtimeConfig.changelogUrl, '_blank'); + popupWindow(runtimeConfig.changelogUrl); onOpenChangelog(); } else { throw new Unreachable(); diff --git a/packages/frontend/core/src/components/pure/help-island/index.tsx b/packages/frontend/core/src/components/pure/help-island/index.tsx index 037354a4d6..79d089d508 100644 --- a/packages/frontend/core/src/components/pure/help-island/index.tsx +++ b/packages/frontend/core/src/components/pure/help-island/index.tsx @@ -1,4 +1,5 @@ import { Tooltip } from '@affine/component/ui/tooltip'; +import { popupWindow } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon, NewIcon } from '@blocksuite/icons'; import { Doc, useLiveData, useServiceOptional } from '@toeverything/infra'; @@ -70,7 +71,7 @@ export const HelpIsland = () => { { - window.open(runtimeConfig.changelogUrl, '_blank'); + popupWindow(runtimeConfig.changelogUrl); }} > diff --git a/packages/frontend/core/src/hooks/use-app-updater.ts b/packages/frontend/core/src/hooks/use-app-updater.ts index 87ae8e62a4..d99fe5b559 100644 --- a/packages/frontend/core/src/hooks/use-app-updater.ts +++ b/packages/frontend/core/src/hooks/use-app-updater.ts @@ -7,7 +7,7 @@ import { atomWithObservable, atomWithStorage } from 'jotai/utils'; import { useCallback, useState } from 'react'; import { Observable } from 'rxjs'; -import { mixpanel } from '../utils'; +import { mixpanel, popupWindow } from '../utils'; import { useAsyncCallback } from './affine-async-hooks'; function rpcToObservable< @@ -191,7 +191,7 @@ export const useAppUpdater = () => { mixpanel.track('Button', { resolve: 'OpenChangelog', }); - window.open(runtimeConfig.changelogUrl, '_blank'); + popupWindow(runtimeConfig.changelogUrl); await setChangelogUnread(true); }, [setChangelogUnread]); diff --git a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx index 4a7f7fccf0..5345671725 100644 --- a/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx +++ b/packages/frontend/core/src/modules/workbench/view/workbench-link.tsx @@ -1,4 +1,5 @@ import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; +import { popupWindow } from '@affine/core/utils'; import { useLiveData, useService } from '@toeverything/infra'; import type { To } from 'history'; import { useCallback } from 'react'; @@ -32,7 +33,7 @@ export const WorkbenchLink = ({ typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`; - window.open(basename + href, '_blank'); + popupWindow(basename + href); } } else { workbench.open(to); diff --git a/packages/frontend/core/src/pages/redirect.tsx b/packages/frontend/core/src/pages/redirect.tsx new file mode 100644 index 0000000000..9b2bd77904 --- /dev/null +++ b/packages/frontend/core/src/pages/redirect.tsx @@ -0,0 +1,43 @@ +import { type LoaderFunction, Navigate, useLoaderData } from 'react-router-dom'; + +const trustedDomain = [ + 'stripe.com', + 'github.com', + 'twitter.com', + 'discord.gg', + 'youtube.com', + 't.me', + 'reddit.com', +]; + +export const loader: LoaderFunction = async ({ request }) => { + const url = new URL(request.url); + const searchParams = url.searchParams; + const redirectUri = searchParams.get('redirect_uri'); + + if (!redirectUri) { + return { allow: false }; + } + + const target = new URL(redirectUri); + + if ( + trustedDomain.some(domain => + new RegExp(`.?${domain}$`).test(target.hostname) + ) + ) { + location.href = redirectUri; + } + + return { allow: true }; +}; + +export const Component = () => { + const { allow } = useLoaderData() as { allow: boolean }; + + if (allow) { + return null; + } + + return ; +}; diff --git a/packages/frontend/core/src/router.tsx b/packages/frontend/core/src/router.tsx index 17642447ad..08dbb6546e 100644 --- a/packages/frontend/core/src/router.tsx +++ b/packages/frontend/core/src/router.tsx @@ -75,6 +75,10 @@ export const topLevelRoutes = [ path: '/onboarding', lazy: () => import('./pages/onboarding'), }, + { + path: '/redirect-proxy', + lazy: () => import('./pages/redirect'), + }, { path: '*', lazy: () => import('./pages/404'), diff --git a/packages/frontend/core/src/utils/index.ts b/packages/frontend/core/src/utils/index.ts index 90e1777b92..07e73f5141 100644 --- a/packages/frontend/core/src/utils/index.ts +++ b/packages/frontend/core/src/utils/index.ts @@ -2,5 +2,6 @@ export * from './create-emotion-cache'; export * from './fractional-indexing'; export * from './intl-formatter'; export * from './mixpanel'; +export * from './popup'; export * from './string2color'; export * from './toast'; diff --git a/packages/frontend/core/src/utils/popup.ts b/packages/frontend/core/src/utils/popup.ts new file mode 100644 index 0000000000..311863b345 --- /dev/null +++ b/packages/frontend/core/src/utils/popup.ts @@ -0,0 +1,6 @@ +export function popupWindow(target: string) { + const url = new URL(runtimeConfig.serverUrlPrefix + '/redirect-proxy'); + url.searchParams.set('redirect_uri', target); + + return window.open(url, '_blank', `noreferrer noopener`); +}