feat(desktop): copy and paste login token

This commit is contained in:
DIYgod 2025-06-03 20:56:57 +08:00
parent 5bbd96ed90
commit 47e07a4cc3
No known key found for this signature in database
7 changed files with 147 additions and 21 deletions

View File

@ -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: () => <TokenModalContent />,
})
}
const Inner = (
<>
<div className="-mt-9 mb-4 flex items-center justify-center">
@ -108,9 +117,26 @@ export const LoginModalContent = (props: LoginModalContentProps) => {
<span>{t("login.continueWith", { provider: provider.name })}</span>
</MotionButtonBase>
))}
<div className="text-text-secondary -mb-1.5 mt-1 text-center text-xs leading-4">
<a onClick={() => handleOpenToken()} className="hover:underline">
{t("login.enter_token")}
</a>
</div>
<div className="text-text-secondary text-center text-xs leading-4">
<span>{t("login.agree_to")}</span>{" "}
<a onClick={() => handleOpenLegal("tos")} className="text-accent hover:underline">
{t("login.terms")}
</a>{" "}
&{" "}
<a onClick={() => handleOpenLegal("privacy")} className="text-accent hover:underline">
{t("login.privacy")}
</a>
</div>
</div>
)}
<Divider className="mb-5 mt-6" />
<Divider className="mb-5 mt-4" />
{isEmail ? (
<div className="flex items-center justify-center pb-2">
<MotionButtonBase
@ -132,23 +158,6 @@ export const LoginModalContent = (props: LoginModalContentProps) => {
/>
</div>
)}
<div className="text-text-secondary mt-3 text-center text-xs leading-5">
<span>{t("login.agree_to")}</span> <br />
<a
onClick={() => handleOpenLegal("tos")}
className="text-accent cursor-pointer hover:underline"
>
{t("login.terms")}
</a>{" "}
&{" "}
<a
onClick={() => handleOpenLegal("privacy")}
className="text-accent cursor-pointer hover:underline"
>
{t("login.privacy")}
</a>
</div>
</>
)
if (isMobile) {
@ -166,7 +175,7 @@ export const LoginModalContent = (props: LoginModalContentProps) => {
<div
onClick={stopPropagation}
tabIndex={-1}
className="bg-background w-[25rem] rounded-xl border p-3 px-8 shadow-2xl shadow-stone-300 dark:border-neutral-700 dark:shadow-stone-800"
className="bg-background w-[26rem] rounded-xl border p-3 px-8 shadow-2xl shadow-stone-300 dark:border-neutral-700 dark:shadow-stone-800"
>
{Inner}
</div>

View File

@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
const { t } = useTranslation("common")
const [isLoading, setIsLoading] = useState(false)
async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true)
const urlObj = new URL(values.token)
const token = urlObj.searchParams.get("token")
if (token) {
await oneTimeToken.apply({ token })
handleSessionChanges()
}
setIsLoading(false)
}
return (
<div className="size-full overflow-hidden">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-[512px] max-w-full overflow-hidden px-0.5"
>
<FormField
control={form.control}
name="token"
render={({ field }) => (
<FormItem className="flex flex-col items-center gap-2 md:block">
<FormControl>
<Input
className="mt-1 dark:text-zinc-200"
placeholder="folo://auth?token=xxx"
{...field}
/>
</FormControl>
<div className="h-6">
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="center relative flex">
<Button
variant="primary"
type="submit"
disabled={!form.formState.isValid}
isLoading={isLoading}
>
{t("words.submit")}
</Button>
</div>
</form>
</Form>
</div>
)
}

View File

@ -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<string>()
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 })}
</Button>
</div>
{openFailed && callbackUrl && (
<div className="text-text mt-8 w-[31rem] space-y-2 text-center text-sm">
<p>{t("login.enter_token")}</p>
<p className="bg-fill-tertiary flex items-center justify-center gap-4 rounded-lg p-3">
<span className="blur-sm hover:blur-none">{callbackUrl}</span>
<i
className="i-mgc-copy-2-cute-re size-4 cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(callbackUrl)
}}
/>
</p>
</div>
)}
</div>
)
}
@ -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: {

View File

@ -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? <strong>Sign in</strong>",
"login.no_account": "Don't have an account? <strong>Sign up</strong>",

View File

@ -247,6 +247,7 @@
"login.confirm_password.label": "确认密码",
"login.continueWith": "使用 {{provider}} 继续",
"login.email": "邮件地址",
"login.enter_token": "输入授权令牌以继续",
"login.forget_password.note": "忘记了密码?",
"login.have_account": "已有账户?<strong>登录</strong>",
"login.no_account": "没有账户?<strong>注册</strong>",

View File

@ -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",

View File

@ -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": "无效的邮箱地址",