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`);
+}