From 47e07a4cc3d77ec68b90b3722a1a5c78e304bc55 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 3 Jun 2025 20:56:57 +0800 Subject: [PATCH] feat(desktop): copy and paste login token --- .../src/modules/auth/LoginModalContent.tsx | 47 ++++++----- .../renderer/src/modules/auth/TokenModal.tsx | 80 +++++++++++++++++++ apps/ssr/client/modules/login/index.tsx | 37 ++++++++- locales/app/en.json | 1 + locales/app/zh-CN.json | 1 + locales/external/en.json | 1 + locales/external/zh-CN.json | 1 + 7 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/auth/TokenModal.tsx diff --git a/apps/desktop/layer/renderer/src/modules/auth/LoginModalContent.tsx b/apps/desktop/layer/renderer/src/modules/auth/LoginModalContent.tsx index daa58cd95..70aac1eab 100644 --- a/apps/desktop/layer/renderer/src/modules/auth/LoginModalContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/auth/LoginModalContent.tsx @@ -16,6 +16,7 @@ import { useAuthProviders } from "~/queries/users" import { LoginWithPassword, RegisterForm } from "./Form" import { LegalModalContent } from "./LegalModal" +import { TokenModalContent } from "./TokenModal" interface LoginModalContentProps { runtime: LoginRuntime @@ -49,6 +50,14 @@ export const LoginModalContent = (props: LoginModalContentProps) => { }) } + const handleOpenToken = () => { + present({ + id: "token", + title: t("login.enter_token"), + content: () => , + }) + } + const Inner = ( <>
@@ -108,9 +117,26 @@ export const LoginModalContent = (props: LoginModalContentProps) => { {t("login.continueWith", { provider: provider.name })} ))} + +
+ handleOpenToken()} className="hover:underline"> + {t("login.enter_token")} + +
+
+ {t("login.agree_to")}{" "} + handleOpenLegal("tos")} className="text-accent hover:underline"> + {t("login.terms")} + {" "} + &{" "} + handleOpenLegal("privacy")} className="text-accent hover:underline"> + {t("login.privacy")} + +
)} - + + {isEmail ? (
{ />
)} - -
- {t("login.agree_to")}
- handleOpenLegal("tos")} - className="text-accent cursor-pointer hover:underline" - > - {t("login.terms")} - {" "} - &{" "} - handleOpenLegal("privacy")} - className="text-accent cursor-pointer hover:underline" - > - {t("login.privacy")} - -
) if (isMobile) { @@ -166,7 +175,7 @@ export const LoginModalContent = (props: LoginModalContentProps) => {
{Inner}
diff --git a/apps/desktop/layer/renderer/src/modules/auth/TokenModal.tsx b/apps/desktop/layer/renderer/src/modules/auth/TokenModal.tsx new file mode 100644 index 000000000..df4d3084e --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/auth/TokenModal.tsx @@ -0,0 +1,80 @@ +import { Button } from "@follow/components/ui/button/index.js" +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@follow/components/ui/form/index.jsx" +import { Input } from "@follow/components/ui/input/index.js" +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { oneTimeToken } from "~/lib/auth" +import { handleSessionChanges } from "~/queries/auth" + +const formSchema = z.object({ + token: z.string().min(1), +}) + +export const TokenModalContent = () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + }) + const { t } = useTranslation("common") + const [isLoading, setIsLoading] = useState(false) + + async function onSubmit(values: z.infer) { + setIsLoading(true) + const urlObj = new URL(values.token) + const token = urlObj.searchParams.get("token") + if (token) { + await oneTimeToken.apply({ token }) + handleSessionChanges() + } + setIsLoading(false) + } + + return ( +
+
+ + ( + + + + +
+ +
+
+ )} + /> +
+ +
+ + +
+ ) +} diff --git a/apps/ssr/client/modules/login/index.tsx b/apps/ssr/client/modules/login/index.tsx index d8a13c297..e65966225 100644 --- a/apps/ssr/client/modules/login/index.tsx +++ b/apps/ssr/client/modules/login/index.tsx @@ -1,5 +1,6 @@ import { UserAvatar } from "@client/components/ui/user-avatar" import { loginHandler, oneTimeToken, signOut, twoFactor } from "@client/lib/auth" +import { openInFollowApp } from "@client/lib/helper" import { queryClient } from "@client/lib/query-client" import { useSession } from "@client/query/auth" import { useAuthProviders } from "@client/query/users" @@ -58,10 +59,18 @@ export function Login() { } }, []) + const [openFailed, setOpenFailed] = useState(false) + const [callbackUrl, setCallbackUrl] = useState() const handleOpenApp = useCallback(async () => { const callbackUrl = await getCallbackUrl() if (!callbackUrl) return - window.open(callbackUrl.url, "_top") + setCallbackUrl(callbackUrl.url) + openInFollowApp({ + deeplink: callbackUrl.url, + fallback: () => { + setOpenFailed(true) + }, + }) }, [getCallbackUrl]) const onceRef = useRef(false) @@ -120,6 +129,20 @@ export function Login() { {t("redirect.openApp", { app_name: APP_NAME })} + {openFailed && callbackUrl && ( +
+

{t("login.enter_token")}

+

+ {callbackUrl} + { + navigator.clipboard.writeText(callbackUrl) + }} + /> +

+
+ )} ) } @@ -179,7 +202,17 @@ export function Login() { ) } } - }, [authProviders, handleOpenApp, isAuthenticated, refetch, t, isEmail, navigate]) + }, [ + authProviders, + handleOpenApp, + isAuthenticated, + refetch, + t, + isEmail, + navigate, + openFailed, + callbackUrl, + ]) const Content = useMemo(() => { switch (true) { case redirecting: { diff --git a/locales/app/en.json b/locales/app/en.json index 2ff7c1c38..6bd8e4d14 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -248,6 +248,7 @@ "login.confirm_password.label": "Confirm Password", "login.continueWith": "Continue with {{provider}}", "login.email": "Email", + "login.enter_token": "Enter authorization token to continue", "login.forget_password.note": "Forgot your password?", "login.have_account": "Already have an account? Sign in", "login.no_account": "Don't have an account? Sign up", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index 911dd0a72..948fad8f7 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -247,6 +247,7 @@ "login.confirm_password.label": "确认密码", "login.continueWith": "使用 {{provider}} 继续", "login.email": "邮件地址", + "login.enter_token": "输入授权令牌以继续", "login.forget_password.note": "忘记了密码?", "login.have_account": "已有账户?登录", "login.no_account": "没有账户?注册", diff --git a/locales/external/en.json b/locales/external/en.json index f9be84f38..9183b2895 100644 --- a/locales/external/en.json +++ b/locales/external/en.json @@ -41,6 +41,7 @@ "login.confirm_password.label": "Confirm Password", "login.continueWith": "Continue with {{provider}}", "login.email": "Email", + "login.enter_token": "If you aren't redirected automatically, copy the token below and paste it in the \"Enter authorization token to continue\" form on the Desktop App.", "login.errors.unknown": "Errors Unknown", "login.forget_password.description": "Enter the email address associated with your account and we'll send you an email about how to reset your password.", "login.forget_password.email_invalid": "Invalid email", diff --git a/locales/external/zh-CN.json b/locales/external/zh-CN.json index c07a4f27c..e8e046983 100644 --- a/locales/external/zh-CN.json +++ b/locales/external/zh-CN.json @@ -41,6 +41,7 @@ "login.confirm_password.label": "确认密码", "login.continueWith": "使用 {{provider}} 登录", "login.email": "邮件地址", + "login.enter_token": "如果未自动重定向,请复制下方令牌并粘贴到桌面应用的“输入授权令牌以继续”表单中。", "login.errors.unknown": "未知错误", "login.forget_password.description": "请输入与你的帐户关联的邮件地址,我们将向你发送一封关于如何重置密码的邮件。", "login.forget_password.email_invalid": "无效的邮箱地址",