feat(desktop): copy and paste login token
This commit is contained in:
parent
5bbd96ed90
commit
47e07a4cc3
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "无效的邮箱地址",
|
||||
|
|
|
|||
Loading…
Reference in New Issue