From ef07fcd810f8702766e6103dfa579998d6232fa5 Mon Sep 17 00:00:00 2001 From: 0xzio Date: Fri, 16 May 2025 23:29:43 +0800 Subject: [PATCH] feat: improve email login --- apps/mobile/src/App.tsx | 6 +- .../src/components/Login/EmailLoginButton.tsx | 42 ----- apps/mobile/src/components/NavContext.tsx | 18 +++ .../{Login => Profile}/AppleLoginButton.tsx | 0 .../components/Profile/EmailLoginButton.tsx | 19 +++ .../{Login => Profile}/GoogleLoginButton.tsx | 0 .../{Login => Profile}/LoginContent.tsx | 5 +- .../{Login => Profile}/LoginForm.tsx | 25 ++- .../src/components/Profile/PinCodeForm.tsx | 133 ++++++++++++++++ .../src/components/Profile/ProfileContent.tsx | 48 ++++++ .../src/components/Profile/RegisterForm.tsx | 144 ++++++++++++++++++ apps/mobile/src/pages/PageEmailLogin.tsx | 44 +++--- apps/mobile/src/pages/PageHome.tsx | 8 +- .../src/useAuthStatus.ts} | 0 packages/widgets/package.json | 1 + .../widgets/src/LoginDialog/LoginDialog.tsx | 2 +- .../src/LoginDialog/LoginDialogContent.tsx | 13 -- .../widgets/src/LoginDialog/LoginForm.tsx | 2 +- .../widgets/src/LoginDialog/PinCodeForm.tsx | 3 +- .../widgets/src/LoginDialog/RegisterForm.tsx | 2 +- pnpm-lock.yaml | 3 + 21 files changed, 429 insertions(+), 89 deletions(-) delete mode 100644 apps/mobile/src/components/Login/EmailLoginButton.tsx create mode 100644 apps/mobile/src/components/NavContext.tsx rename apps/mobile/src/components/{Login => Profile}/AppleLoginButton.tsx (100%) create mode 100644 apps/mobile/src/components/Profile/EmailLoginButton.tsx rename apps/mobile/src/components/{Login => Profile}/GoogleLoginButton.tsx (100%) rename apps/mobile/src/components/{Login => Profile}/LoginContent.tsx (89%) rename apps/mobile/src/components/{Login => Profile}/LoginForm.tsx (83%) create mode 100644 apps/mobile/src/components/Profile/PinCodeForm.tsx create mode 100644 apps/mobile/src/components/Profile/ProfileContent.tsx create mode 100644 apps/mobile/src/components/Profile/RegisterForm.tsx rename packages/{widgets/src/LoginDialog/useAuthStatus.tsx => hooks/src/useAuthStatus.ts} (100%) diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index d0c6216b..c8ea98ef 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -46,6 +46,7 @@ import { useEffect, useRef } from 'react' import { appEmitter } from '@penx/emitter' import { useCreationId } from '@penx/hooks/useCreationId' import { ICreationNode } from '@penx/model-type' +import { NavProvider } from './components/NavContext' import { PageCreation } from './pages/PageCreation' async function init() { @@ -120,7 +121,6 @@ setupIonicReact() const App: React.FC = () => { const nav = useRef(null) - const { creationId, setCreationId } = useCreationId() useEffect(() => { // if (initRef.current) return @@ -153,7 +153,9 @@ const App: React.FC = () => { - }> + + }> + diff --git a/apps/mobile/src/components/Login/EmailLoginButton.tsx b/apps/mobile/src/components/Login/EmailLoginButton.tsx deleted file mode 100644 index d9ab6c4d..00000000 --- a/apps/mobile/src/components/Login/EmailLoginButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import React, { useMemo, useState } from 'react' -import { PageEmailLogin } from '@/pages/PageEmailLogin' -import { Capacitor } from '@capacitor/core' -import { SocialLogin } from '@capgo/capacitor-social-login' -import { IonButton, IonNavLink, useIonRouter } from '@ionic/react' -import { set } from 'idb-keyval' -import { MailIcon } from 'lucide-react' -import { appEmitter } from '@penx/emitter' -import { localDB } from '@penx/local-db' -import { queryClient } from '@penx/query-client' -import { useSession } from '@penx/session' -import { MobileGoogleLoginInfo } from '@penx/types' -import { Button } from '@penx/uikit/button' -import { IconGoogle } from '@penx/uikit/IconGoogle' -import { LoadingDots } from '@penx/uikit/loading-dots' -import { LoginForm } from './LoginForm' - -interface Props {} -export function EmailLoginButton({}: Props) { - const { login } = useSession() - const [json, setJson] = useState({}) - const [error, setError] = useState({}) - const [session, setSession] = useState({}) - const [loading, setLoading] = useState(false) - const router = useIonRouter() - - return ( - }> - - - ) -} diff --git a/apps/mobile/src/components/NavContext.tsx b/apps/mobile/src/components/NavContext.tsx new file mode 100644 index 00000000..13264e98 --- /dev/null +++ b/apps/mobile/src/components/NavContext.tsx @@ -0,0 +1,18 @@ +'use client' + +import { createContext, PropsWithChildren, useContext } from 'react' + +export const NavContext = createContext({} as HTMLIonNavElement) + +interface Props { + nav: HTMLIonNavElement +} + +export const NavProvider = ({ nav, children }: PropsWithChildren) => { + return {children} +} + +export function useNavContext() { + const nav = useContext(NavContext) + return nav +} diff --git a/apps/mobile/src/components/Login/AppleLoginButton.tsx b/apps/mobile/src/components/Profile/AppleLoginButton.tsx similarity index 100% rename from apps/mobile/src/components/Login/AppleLoginButton.tsx rename to apps/mobile/src/components/Profile/AppleLoginButton.tsx diff --git a/apps/mobile/src/components/Profile/EmailLoginButton.tsx b/apps/mobile/src/components/Profile/EmailLoginButton.tsx new file mode 100644 index 00000000..884c6925 --- /dev/null +++ b/apps/mobile/src/components/Profile/EmailLoginButton.tsx @@ -0,0 +1,19 @@ +'use client' + +import React from 'react' +import { PageEmailLogin } from '@/pages/PageEmailLogin' +import { IonNavLink } from '@ionic/react' +import { MailIcon } from 'lucide-react' +import { Button } from '@penx/uikit/button' + +interface Props {} +export function EmailLoginButton({}: Props) { + return ( + }> + + + ) +} diff --git a/apps/mobile/src/components/Login/GoogleLoginButton.tsx b/apps/mobile/src/components/Profile/GoogleLoginButton.tsx similarity index 100% rename from apps/mobile/src/components/Login/GoogleLoginButton.tsx rename to apps/mobile/src/components/Profile/GoogleLoginButton.tsx diff --git a/apps/mobile/src/components/Login/LoginContent.tsx b/apps/mobile/src/components/Profile/LoginContent.tsx similarity index 89% rename from apps/mobile/src/components/Login/LoginContent.tsx rename to apps/mobile/src/components/Profile/LoginContent.tsx index 48007962..9fec2689 100644 --- a/apps/mobile/src/components/Login/LoginContent.tsx +++ b/apps/mobile/src/components/Profile/LoginContent.tsx @@ -24,7 +24,10 @@ const platform = Capacitor.getPlatform() export function LoginContent({}: Props) { return (
-
+
+
+ Welcome to PenX +
{platform === 'ios' && } diff --git a/apps/mobile/src/components/Login/LoginForm.tsx b/apps/mobile/src/components/Profile/LoginForm.tsx similarity index 83% rename from apps/mobile/src/components/Login/LoginForm.tsx rename to apps/mobile/src/components/Profile/LoginForm.tsx index 9b2b4fed..933dcb96 100644 --- a/apps/mobile/src/components/Login/LoginForm.tsx +++ b/apps/mobile/src/components/Profile/LoginForm.tsx @@ -8,7 +8,7 @@ import { set } from 'idb-keyval' import { toast } from 'sonner' import { z } from 'zod' import { appEmitter } from '@penx/emitter' -import { useRouter } from '@penx/libs/i18n' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' import { queryClient } from '@penx/query-client' import { useSession } from '@penx/session' import { Button } from '@penx/uikit/button' @@ -22,11 +22,10 @@ import { import { Input } from '@penx/uikit/input' import { LoadingDots } from '@penx/uikit/loading-dots' import { extractErrorMessage } from '@penx/utils/extractErrorMessage' +import { useNavContext } from '../NavContext' const FormSchema = z.object({ - name: z.string().min(4, { - message: 'Username must be at least 4 characters.', - }), + name: z.string().email(), password: z.string().min(4, { message: 'Password must be at least 4 characters.', }), @@ -37,6 +36,8 @@ interface Props {} export function LoginForm({}: Props) { const [isLoading, setLoading] = useState(false) const { login } = useSession() + const { setAuthStatus } = useAuthStatus() + const nav = useNavContext() const form = useForm>({ resolver: zodResolver(FormSchema), @@ -47,9 +48,9 @@ export function LoginForm({}: Props) { }) async function onSubmit(data: z.infer) { + console.log('=====>>>>>>>nav:', nav); + try { - console.log('======data:', data) - setLoading(true) const session = await login({ @@ -65,6 +66,7 @@ export function LoginForm({}: Props) { await set('SESSION', session) queryClient.setQueryData(['SESSION'], session) appEmitter.emit('APP_LOGIN_SUCCESS', session) + nav.pop() } } catch (error) { console.log('========error:', error) @@ -102,6 +104,7 @@ export function LoginForm({}: Props) {
+ +
+ ?{' '} + setAuthStatus({ type: 'register' })} + > + + +
) } diff --git a/apps/mobile/src/components/Profile/PinCodeForm.tsx b/apps/mobile/src/components/Profile/PinCodeForm.tsx new file mode 100644 index 00000000..86ad4a0d --- /dev/null +++ b/apps/mobile/src/components/Profile/PinCodeForm.tsx @@ -0,0 +1,133 @@ +'use client' + +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { Trans } from '@lingui/react' +import { set } from 'idb-keyval' +import { useSearchParams } from 'next/navigation' +import { toast } from 'sonner' +import { z } from 'zod' +import { appEmitter } from '@penx/emitter' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' +import { localDB } from '@penx/local-db' +import { queryClient } from '@penx/query-client' +import { useSession } from '@penx/session' +import { api } from '@penx/trpc-client' +import { Button } from '@penx/uikit/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@penx/uikit/form' +import { Input } from '@penx/uikit/input' +import { LoadingDots } from '@penx/uikit/loading-dots' +import { extractErrorMessage } from '@penx/utils/extractErrorMessage' +import { useNavContext } from '../NavContext' + +const FormSchema = z.object({ + code: z.string(), +}) + +interface Props {} + +export function PinCodeForm({}: Props) { + const { login } = useSession() + const [isLoading, setLoading] = useState(false) + const { authStatus } = useAuthStatus() + const nav = useNavContext() + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + code: '', + }, + }) + + async function onSubmit(data: z.infer) { + try { + setLoading(true) + const session = await login({ + type: 'register-by-code', + code: data.code, + }) + console.log('=====session:', session) + + if (!session.isLoggedIn) { + toast.error(session.message) + } else { + await set('SESSION', session) + queryClient.setQueryData(['SESSION'], session) + appEmitter.emit('APP_LOGIN_SUCCESS', session) + nav.pop() + } + } catch (error) { + console.log('========error:', error) + const msg = extractErrorMessage(error) + toast.error(msg) + } + + setLoading(false) + } + + return ( +
+ +

+ +

+ + ( + + + + + + + + + + + + + )} + /> + +
+ +
+ + + ) +} diff --git a/apps/mobile/src/components/Profile/ProfileContent.tsx b/apps/mobile/src/components/Profile/ProfileContent.tsx new file mode 100644 index 00000000..c076dd82 --- /dev/null +++ b/apps/mobile/src/components/Profile/ProfileContent.tsx @@ -0,0 +1,48 @@ +'use client' + +import React, { useMemo, useState } from 'react' +import { Capacitor } from '@capacitor/core' +import { SocialLogin } from '@capgo/capacitor-social-login' +import { LogOutIcon } from 'lucide-react' +import { useSession } from '@penx/session' +import { Avatar, AvatarFallback, AvatarImage } from '@penx/uikit/avatar' +import { cn, getUrl } from '@penx/utils' +import { generateGradient } from '@penx/utils/generateGradient' + +interface Props {} + +const platform = Capacitor.getPlatform() + +export function ProfileContent({}: Props) { + const { session, logout } = useSession() + return ( +
+
+ + + + {session?.name.slice(0, 1)} + + +
+ {session?.name} + {session?.email} +
+
+
{ + logout() + }} + > +
Logout
+ +
+
+ ) +} diff --git a/apps/mobile/src/components/Profile/RegisterForm.tsx b/apps/mobile/src/components/Profile/RegisterForm.tsx new file mode 100644 index 00000000..961f21a8 --- /dev/null +++ b/apps/mobile/src/components/Profile/RegisterForm.tsx @@ -0,0 +1,144 @@ +'use client' + +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { Trans } from '@lingui/react' +import { useSearchParams } from 'next/navigation' +import { toast } from 'sonner' +import { z } from 'zod' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' +import { localDB } from '@penx/local-db' +import { api } from '@penx/trpc-client' +import { Button } from '@penx/uikit/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@penx/uikit/form' +import { Input } from '@penx/uikit/input' +import { LoadingDots } from '@penx/uikit/loading-dots' +import { extractErrorMessage } from '@penx/utils/extractErrorMessage' + +const FormSchema = z.object({ + email: z.string().email(), + password: z.string().min(6, { + message: 'Password must be at least 6 characters.', + }), +}) + +interface Props {} + +export function RegisterForm({}: Props) { + const [isLoading, setLoading] = useState(false) + const { setAuthStatus } = useAuthStatus() + const searchParams = useSearchParams() + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + email: '', + password: '', + }, + }) + + async function onSubmit(data: z.infer) { + try { + const sites = await localDB.listAllSites() + const site = sites.find((s) => !s.props.isRemote) + + const ref = searchParams?.get('ref') as string + setLoading(true) + await api.auth.registerByEmail.mutate({ + ...data, + ref: ref || '', + userId: site?.userId, + }) + + setAuthStatus({ + type: 'register-email-sent', + data: { + ...data, + ref: ref || '', + userId: site?.userId, + }, + }) + } catch (error) { + console.log('========error:', error) + const msg = extractErrorMessage(error) + toast.error(msg) + } + + setLoading(false) + } + + return ( +
+ + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + +
+ +
+ + +
+ ?{' '} + { + e.preventDefault() + setAuthStatus({ type: 'login' }) + }} + > + + +
+ + ) +} diff --git a/apps/mobile/src/pages/PageEmailLogin.tsx b/apps/mobile/src/pages/PageEmailLogin.tsx index ea3ba436..959393ad 100644 --- a/apps/mobile/src/pages/PageEmailLogin.tsx +++ b/apps/mobile/src/pages/PageEmailLogin.tsx @@ -1,4 +1,7 @@ import React from 'react' +import { LoginForm } from '@/components/Profile/LoginForm' +import { PinCodeForm } from '@/components/Profile/PinCodeForm' +import { RegisterForm } from '@/components/Profile/RegisterForm' import { Capacitor } from '@capacitor/core' import { IonBackButton, @@ -10,42 +13,45 @@ import { IonTitle, IonToolbar, } from '@ionic/react' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' const platform = Capacitor.getPlatform() export function PageEmailLogin() { + const { authStatus } = useAuthStatus() return ( <> - + - Page Two + Email login -

Page Two

- {/* }> - Go to Page Three - */} +
+ {authStatus.type === 'login' && } + + {authStatus.type === 'register' && } + + {authStatus.type === 'register-email-sent' && ( + + )} +
) diff --git a/apps/mobile/src/pages/PageHome.tsx b/apps/mobile/src/pages/PageHome.tsx index 81c7aeb8..63f51a8c 100644 --- a/apps/mobile/src/pages/PageHome.tsx +++ b/apps/mobile/src/pages/PageHome.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useRef, useState } from 'react' import { Footer } from '@/components/Footer' -import { LoginContent } from '@/components/Login/LoginContent' import { MobileHome } from '@/components/MobileHome' import { SearchButton } from '@/components/MobileSearch/SearchButton' import { MobileTask } from '@/components/MobileTask/MobileTask' +import { LoginContent } from '@/components/Profile/LoginContent' +import { ProfileContent } from '@/components/Profile/ProfileContent' import { useHomeTab } from '@/hooks/useHomeTab' import { Capacitor } from '@capacitor/core' import { SplashScreen } from '@capacitor/splash-screen' @@ -40,6 +41,7 @@ import { appEmitter } from '@penx/emitter' import { useArea } from '@penx/hooks/useArea' import { useCreationId } from '@penx/hooks/useCreationId' import { ICreationNode } from '@penx/model-type' +import { useSession } from '@penx/session' import { Button } from '@penx/uikit/button' import { Separator } from '@penx/uikit/separator' import { cn } from '@penx/utils' @@ -53,6 +55,7 @@ const PageHome: React.FC = ({ nav }: any) => { const { area } = useArea() const y = useMotionValue(0) const { isHome, type, setType } = useHomeTab() + const { session } = useSession() const inputRef = useRef(null) @@ -207,7 +210,8 @@ const PageHome: React.FC = ({ nav }: any) => { > {type === 'HOME' && } {type === 'TASK' && } - {type === 'PROFILE' && } + {type === 'PROFILE' && + (session ? : )}
diff --git a/packages/widgets/src/LoginDialog/useAuthStatus.tsx b/packages/hooks/src/useAuthStatus.ts similarity index 100% rename from packages/widgets/src/LoginDialog/useAuthStatus.tsx rename to packages/hooks/src/useAuthStatus.ts diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 251094bb..76e30def 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -35,6 +35,7 @@ "@penx/local-db": "workspace:*", "@penx/model-type": "workspace:*", "@penx/domain": "workspace:*", + "@penx/hooks": "workspace:*", "@penx/query-client": "workspace:*", "@penx/editor-transforms": "workspace:*", "@penx/contexts": "workspace:*", diff --git a/packages/widgets/src/LoginDialog/LoginDialog.tsx b/packages/widgets/src/LoginDialog/LoginDialog.tsx index 96a4ecee..f898d9dc 100644 --- a/packages/widgets/src/LoginDialog/LoginDialog.tsx +++ b/packages/widgets/src/LoginDialog/LoginDialog.tsx @@ -1,6 +1,7 @@ 'use client' import { Trans } from '@lingui/react' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' import { Dialog, DialogContent, @@ -11,7 +12,6 @@ import { import { LoginDialogContent } from './LoginDialogContent' import { PinCodeForm } from './PinCodeForm' import { RegisterForm } from './RegisterForm' -import { useAuthStatus } from './useAuthStatus' import { useLoginDialog } from './useLoginDialog' interface Props {} diff --git a/packages/widgets/src/LoginDialog/LoginDialogContent.tsx b/packages/widgets/src/LoginDialog/LoginDialogContent.tsx index dde27b4f..4011cc22 100644 --- a/packages/widgets/src/LoginDialog/LoginDialogContent.tsx +++ b/packages/widgets/src/LoginDialog/LoginDialogContent.tsx @@ -1,14 +1,5 @@ 'use client' -import { useCallback } from 'react' -import { - AuthKitProvider, - SignInButton as FSignInButton, - QRCode, - StatusAPIResponse, - useProfile, - useSignIn, -} from '@farcaster/auth-kit' import { Trans } from '@lingui/react' import { useSearchParams } from 'next/navigation' import { toast } from 'sonner' @@ -19,10 +10,6 @@ import { LoginForm } from './LoginForm' import { useLoginDialog } from './useLoginDialog' export function LoginDialogContent() { - const { setIsOpen } = useLoginDialog() - const { login, logout } = useSession() - const searchParams = useSearchParams() - return (
diff --git a/packages/widgets/src/LoginDialog/LoginForm.tsx b/packages/widgets/src/LoginDialog/LoginForm.tsx index 5c53e3d7..d5708441 100644 --- a/packages/widgets/src/LoginDialog/LoginForm.tsx +++ b/packages/widgets/src/LoginDialog/LoginForm.tsx @@ -6,6 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Trans } from '@lingui/react' import { toast } from 'sonner' import { z } from 'zod' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' import { useRouter } from '@penx/libs/i18n' import { useSession } from '@penx/session' import { Button } from '@penx/uikit/button' @@ -19,7 +20,6 @@ import { import { Input } from '@penx/uikit/input' import { LoadingDots } from '@penx/uikit/loading-dots' import { extractErrorMessage } from '@penx/utils/extractErrorMessage' -import { useAuthStatus } from './useAuthStatus' import { useLoginDialog } from './useLoginDialog' const FormSchema = z.object({ diff --git a/packages/widgets/src/LoginDialog/PinCodeForm.tsx b/packages/widgets/src/LoginDialog/PinCodeForm.tsx index c5a26d5d..03e56b13 100644 --- a/packages/widgets/src/LoginDialog/PinCodeForm.tsx +++ b/packages/widgets/src/LoginDialog/PinCodeForm.tsx @@ -7,6 +7,7 @@ import { Trans } from '@lingui/react' import { useSearchParams } from 'next/navigation' import { toast } from 'sonner' import { z } from 'zod' +import { useAuthStatus } from '@penx/hooks/useAuthStatus' import { localDB } from '@penx/local-db' import { useSession } from '@penx/session' import { api } from '@penx/trpc-client' @@ -28,7 +29,6 @@ import { InputOTPSlot, } from '@penx/uikit/ui/input-otp' import { extractErrorMessage } from '@penx/utils/extractErrorMessage' -import { useAuthStatus } from './useAuthStatus' import { useLoginDialog } from './useLoginDialog' const FormSchema = z.object({ @@ -91,6 +91,7 @@ export function PinCodeForm({}: Props) {