diff --git a/packages/auth/components/email-password-form.tsx b/packages/auth/components/email-password-form.tsx index 8ef98192..c651f21e 100644 --- a/packages/auth/components/email-password-form.tsx +++ b/packages/auth/components/email-password-form.tsx @@ -27,11 +27,13 @@ const EmailPasswordForm: React.FC<{ isLoading: boolean className?: string inputClx?: string + prompt?: string }> = ({ onSubmit, isLoading, className, - inputClx + inputClx, + prompt='Login' }) => { const form = useForm>({ resolver: zodResolver(formSchema), @@ -82,7 +84,7 @@ const EmailPasswordForm: React.FC<{ )} /> - + ) diff --git a/packages/auth/components/index.ts b/packages/auth/components/index.ts index dc3cb92e..a92e1b45 100644 --- a/packages/auth/components/index.ts +++ b/packages/auth/components/index.ts @@ -1,3 +1,4 @@ export { default as LoginPanel } from './login-panel' export { default as EmailPasswordForm } from './email-password-form' -export { default as AuthWidget } from './auth-widget' \ No newline at end of file +export { default as AuthWidget } from './auth-widget' +export { default as SignupPanel } from './signup-panel' \ No newline at end of file diff --git a/packages/auth/components/login-panel.tsx b/packages/auth/components/login-panel.tsx index 8173e1d6..1abba6bc 100644 --- a/packages/auth/components/login-panel.tsx +++ b/packages/auth/components/login-panel.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation' import { observer } from 'mobx-react-lite' import Link from 'next/link' -import { ApplyTypography, Button, Separator, toast } from '@hanzo/ui/primitives' +import { ApplyTypography, Button, Separator, toast, Toaster } from '@hanzo/ui/primitives' import { cn } from '@hanzo/ui/util' import { useAuth, type AuthProvider } from '../service' @@ -23,18 +23,18 @@ const ProviderLoginButton: React.FC { - - return ( - - ) -} + + return ( + + ) + } const LoginPanel: React.FC void termsOfServiceUrl?: string privacyPolicyUrl?: string + setIsLogin?: React.Dispatch> }> = observer(({ children, redirectUrl, @@ -54,14 +55,15 @@ const LoginPanel: React.FC { const router = useRouter() const auth = useAuth() const [isLoading, setIsLoading] = useState(false) - const succeed = async (loginMethod: AuthProvider | 'email' | null ) => { + const succeed = async (loginMethod: AuthProvider | 'email' | null) => { if (loginMethod) { sendGAEvent('login', { method: loginMethod }) @@ -86,8 +88,13 @@ const LoginPanel: React.FC { setIsLoading(true) const res = await auth.loginWithProvider(provider) - if (res.success) { succeed(provider) } + if (res.success) { + succeed(provider) + } setIsLoading(false) } const logout = async () => { setIsLoading(true) const res = await auth.logout() - if (res.success) { succeed(null) } + if (res.success) { + succeed(null) + } setIsLoading(false) } + const handleOnClick = () => { + if (setIsLogin) { + setIsLogin(false) + } + } + return ( {auth.loggedIn && !redirectUrl ? ( @@ -133,7 +150,7 @@ const LoginPanel: React.FCYou are signed in as {auth.user?.displayName ?? auth.user?.email}

{getStartedUrl && } - +
)} @@ -143,25 +160,31 @@ const LoginPanel: React.FCLogin )} {children} - + + +
+
Don't have an account?
+ +
-
or continue with
+
or continue with
{/* */} - Google + Google - Facebook + Facebook - Github + Github

By logging in, you agree to our Terms of Service and Privacy Policy.

+ )}
diff --git a/packages/auth/components/signup-panel.tsx b/packages/auth/components/signup-panel.tsx new file mode 100644 index 00000000..d6c77b2f --- /dev/null +++ b/packages/auth/components/signup-panel.tsx @@ -0,0 +1,200 @@ +'use client' +import { useState, type PropsWithChildren } from 'react' +import { useRouter } from 'next/navigation' +import { observer } from 'mobx-react-lite' +import Link from 'next/link' + +import { ApplyTypography, Button, Separator, toast, Toaster } from '@hanzo/ui/primitives' +import { cn } from '@hanzo/ui/util' + +import { useAuth, type AuthProvider } from '../service' +import { Facebook, Google, GitHub } from '../icons' +import EmailPasswordForm from './email-password-form' +import { sendGAEvent } from '../util/analytics' + + +const ProviderLoginButton: React.FC Promise, + isLoading: boolean +}> = ({ + provider, + loginWithProvider, + isLoading, + children +}) => { + + return ( + + ) + } + +const SignupPanel: React.FC void + termsOfServiceUrl?: string + privacyPolicyUrl?: string + setIsLogin?: React.Dispatch> +}> = observer(({ + children, + redirectUrl, + getStartedUrl, + className, + inputClx, + noHeading, + onLoginChanged, + termsOfServiceUrl, + privacyPolicyUrl, + setIsLogin +}) => { + + const router = useRouter() + const auth = useAuth() + const [isLoading, setIsLoading] = useState(false) + + const succeed = async (loginMethod: AuthProvider | 'email' | null) => { + + if (loginMethod) { + sendGAEvent('login', { method: loginMethod }) + } + + // If a callback is provided, don't redirect. + // Assume host code is handling (eg, mobile menu) + if (onLoginChanged) { + const res = await fetch( + '/api/auth/generate-custom-token', + { method: 'POST' } + ).then(res => res.json()) + onLoginChanged(res.token?.token ?? null) + } + else if (redirectUrl) { + // TODO :aa shouldn't the token thing happen in this case too?? + router.push(redirectUrl) + } + } + + const signupWithEmailPassword = async (email: string, password: string) => { + setIsLoading(true) + try { + const res = await auth.signupEmailAndPassword(email, password) + if (res.success) { + succeed('email') + toast.success(res.message) + } + else { + toast.error(res.message) + } + } + catch (e) { + toast.success('User with this email already signed up') + } + setIsLoading(false) + } + + // const loginWithEthereum = async () => { + // setIsLoading(true) + // try { + // const res = await signInWithEthereum() + // if (res.success && res.user) { + // setUser({email: res.user?.email, displayName: res.user?.displayName, walletAddress: res.user?.walletAddress}) + // if (redirectUrl) { + // router.push(redirectUrl) + // } + // } + // } catch (e) { + // toast({title: 'No Ethereum provider found'}) + // } + // setIsLoading(false) + // } + + const loginWithProvider = async (provider: AuthProvider) => { + setIsLoading(true) + const res = await auth.loginWithProvider(provider) + if (res.success) { + succeed(provider) + } + setIsLoading(false) + } + + const logout = async () => { + setIsLoading(true) + const res = await auth.logout() + if (res.success) { + succeed(null) + } + setIsLoading(false) + } + + const handleOnClick = () => { + if (setIsLogin) { + setIsLogin(true) + } + } + + return ( + + {auth.loggedIn && !redirectUrl ? ( + <> +

Welcome!

+ {auth.user && (<> {/* this means the hanzo user isn't loaded yet ...*/} +

You are signed in as {auth.user?.displayName ?? auth.user?.email}

+
+ {getStartedUrl && } + +
+ )} + + ) : ( + <> + {!noHeading && ( +

Sign Up

+ )} + {children} + +
+
Already have an account?
+ +
+ +
+
or continue with
+
+ + {/* */} + + Google + + + Facebook + + + Github + +

By logging in, you agree to our Terms of Service and Privacy Policy.

+ + + )} +
+ ) +}) + +export default SignupPanel diff --git a/packages/auth/package.json b/packages/auth/package.json index 6218c935..9bbec5a5 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@hanzo/auth", - "version": "2.4.12", + "version": "2.4.14", "description": "Library with Firebase authentication.", "publishConfig": { "registry": "https://registry.npmjs.org/", diff --git a/packages/auth/service/auth-service.ts b/packages/auth/service/auth-service.ts index 3f2c83fa..bfc5a7d2 100644 --- a/packages/auth/service/auth-service.ts +++ b/packages/auth/service/auth-service.ts @@ -7,7 +7,8 @@ interface AuthService { get loggedIn(): boolean get user(): HanzoUserInfo | null // returns current info obj // all fields observable :) - loginEmailAndPassword: ( email: string, password: string ) => Promise<{success: boolean, userInfo: HanzoUserInfo | null}> + signupEmailAndPassword: ( email: string, password: string ) => Promise<{success: boolean, userInfo: HanzoUserInfo | null, message?: string}> + loginEmailAndPassword: ( email: string, password: string ) => Promise<{success: boolean, userInfo: HanzoUserInfo | null, message?: string}> loginWithProvider: ( provider: AuthProvider ) => Promise<{success: boolean, userInfo: HanzoUserInfo | null}> loginWithCustomToken: ( token: string ) => Promise<{success: boolean, userInfo: HanzoUserInfo | null}> associateWallet: () => Promise diff --git a/packages/auth/service/impl/firebase-support.ts b/packages/auth/service/impl/firebase-support.ts index bf877ff3..78f86c56 100644 --- a/packages/auth/service/impl/firebase-support.ts +++ b/packages/auth/service/impl/firebase-support.ts @@ -10,7 +10,7 @@ import { signInWithCustomToken, } from 'firebase/auth' -import { initializeApp, getApps } from "firebase/app" +import { initializeApp, getApps, FirebaseError } from "firebase/app" import { getAuth } from "firebase/auth" import { getFirestore } from 'firebase/firestore' @@ -27,10 +27,10 @@ export const firebaseConfig = { const firebaseApp = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0] export const auth = getAuth(firebaseApp) - // :aa TODO should be in module conf in host app -export const db = getFirestore(firebaseApp, 'lux-accounts') +// :aa TODO should be in module conf in host app +export const db = getFirestore(firebaseApp, 'lux-accounts') -export async function loginWithProvider(provider: string): Promise<{success: boolean, user: User | null}> { +export async function loginWithProvider(provider: string): Promise<{ success: boolean, user: User | null }> { const authProvider = (() => { switch (provider) { case 'google': @@ -45,7 +45,7 @@ export async function loginWithProvider(provider: string): Promise<{success: boo })() if (!authProvider) { - return {success: false, user: null} + return { success: false, user: null } } try { @@ -60,16 +60,16 @@ export async function loginWithProvider(provider: string): Promise<{success: boo const resBody = (await response.json()) as unknown as APIResponse if (response.ok && resBody.success) { -// const walletAddress = await getAssociatedWalletAddress(userCreds.user.email ?? '') - return {success: true, user: userCreds.user /*.email ? {email: userCreds.user.email, displayName: userCreds.user.displayName ?? undefined, walletAddress: walletAddress.result}: null */} - } + // const walletAddress = await getAssociatedWalletAddress(userCreds.user.email ?? '') + return { success: true, user: userCreds.user /*.email ? {email: userCreds.user.email, displayName: userCreds.user.displayName ?? undefined, walletAddress: walletAddress.result}: null */ } + } else { - return {success: false, user: null} + return { success: false, user: null } } - } + } catch (error) { console.error('Error signing in with Google', error) - return {success: false, user: null} + return { success: false, user: null } } } @@ -93,29 +93,68 @@ export async function signInWithEthereum(opts?: { siteName?: string }): Promise< } */ const isAuthUserNotFound = (e: any) => ( - typeof e === 'object' && - e !== null && - e.hasOwnProperty('code') && + typeof e === 'object' && + e !== null && + e.hasOwnProperty('code') && e.code === 'auth/user-not-found' ) +export async function signupWithEmailAndPassword( + email: string, + password: string +): Promise<{ success: boolean, user?: User, message?: string }> { + + let user: User | undefined = undefined + try { + const userCredential = await createUserWithEmailAndPassword(auth, email, password) + user = userCredential.user + } + catch (error) { + if (error instanceof FirebaseError) { + console.error(error.code) + return {success: false, message: error.code as string} + } + return {success: false, message: error as string} + } + + try { + const idToken = await user.getIdToken() + + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idToken }), + }) + const resBody = (await response.json()) as unknown as APIResponse + + if (response.ok && resBody.success) { + return { success: true, user } + } + else { + return { success: false } + } + } + catch (error) { + console.error('Error signing in with Firebase auth', error) + return { success: false } + } +} + export async function loginWithEmailAndPassword( - email: string, + email: string, password: string -): Promise<{success: boolean, user?: User }> { +): Promise<{ success: boolean, user?: User, message?: string }> { let user: User | undefined = undefined try { const userCredential = await signInWithEmailAndPassword(auth, email, password) user = userCredential.user - } catch (e) { - if (isAuthUserNotFound(e)) { - const userCredential = await createUserWithEmailAndPassword(auth, email, password) - user = userCredential.user - } - else { - throw e + } catch (error) { + if (error instanceof FirebaseError) { + console.error(error.code) + return {success: false, message: error.code as string} } + return {success: false, message: error as string} } try { @@ -123,27 +162,27 @@ export async function loginWithEmailAndPassword( const response = await fetch('/api/auth/login', { method: 'POST', - headers: { 'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), }) const resBody = (await response.json()) as unknown as APIResponse if (response.ok && resBody.success) { - return { success: true, user } - } + return { success: true, user, message: "Login Successfully!" } + } else { - return {success: false} + return { success: false , message: "Login API Failed"} } - } + } catch (error) { console.error('Error signing in with Firebase auth', error) - return {success: false} + return { success: false, message: "Error signing in with Firebase auth" } } } export async function loginWithCustomToken( - token: string, -): Promise<{success: boolean, user?: User }> { + token: string, +): Promise<{ success: boolean, user?: User }> { let user: User | undefined = undefined const userCredential = await signInWithCustomToken(auth, token) @@ -154,40 +193,40 @@ export async function loginWithCustomToken( const response = await fetch('/api/auth/login', { method: 'POST', - headers: { 'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), }) const resBody = (await response.json()) as unknown as APIResponse if (response.ok && resBody.success) { return { success: true, user } - } + } else { - return {success: false} + return { success: false } } - } + } catch (error) { console.error('Error signing in with Firebase auth', error) - return {success: false} + return { success: false } } } -export async function logoutBackend(): Promise<{success: boolean}> { +export async function logoutBackend(): Promise<{ success: boolean }> { try { - const response = await fetch('/api/auth/logout', { headers: {'Content-Type': 'application/json' } }) + const response = await fetch('/api/auth/logout', { headers: { 'Content-Type': 'application/json' } }) const resBody = (await response.json()) as unknown as APIResponse if (response.ok && resBody.success) { - return {success: true} - } + return { success: true } + } else { - return {success: false} + return { success: false } } - } + } catch (error) { console.error('Error logging on on server with Firebase', error) - return {success: false} + return { success: false } } } diff --git a/packages/auth/service/impl/index.ts b/packages/auth/service/impl/index.ts index 2e43b8bc..bd2418fd 100644 --- a/packages/auth/service/impl/index.ts +++ b/packages/auth/service/impl/index.ts @@ -5,6 +5,7 @@ import type { AuthServiceConf, HanzoUserInfo, HanzoUserInfoValue } from '../../t import { auth as fbAuth, + signupWithEmailAndPassword, loginWithCustomToken, loginWithEmailAndPassword, loginWithProvider, @@ -71,10 +72,44 @@ class AuthServiceImpl implements AuthService { ) } + signupEmailAndPassword = async ( + email: string, + password: string + ): Promise<{success: boolean, userInfo: HanzoUserInfo | null, message?: string}> => { + + try { + this._hzUser.clear() + const res = await signupWithEmailAndPassword(email, password) + if (res.success && res.user) { + const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined + this._hzUser.set({ + email: res.user.email ?? '', + displayName : res.user.displayName ?? null, + walletAddress : walletAddress?.result ?? null + }) + + return { + success: true, + userInfo: this._hzUser, + message: res.message + } + } + return { + success: false, + userInfo: null, + message: res.message + } + } + catch (e) { + console.error('Error signing in with Firebase auth', e) + return {success: false, userInfo: null, message: 'Error signing in with Firebase auth'} + } + } + loginEmailAndPassword = async ( email: string, password: string - ): Promise<{success: boolean, userInfo: HanzoUserInfo | null}> => { + ): Promise<{success: boolean, userInfo: HanzoUserInfo | null, message?: string}> => { try { this._hzUser.clear() @@ -89,17 +124,19 @@ class AuthServiceImpl implements AuthService { return { success: true, - userInfo: this._hzUser + userInfo: this._hzUser, + message: res.message } } return { success: false, - userInfo: null + userInfo: null, + message: res.message } } catch (e) { console.error('Error signing in with Firebase auth', e) - return {success: false, userInfo: null} + return {success: false, userInfo: null, message: 'Error signing in with Firebase auth'} } }