Skip to content
This repository was archived by the owner on Jan 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions app/components/welcome/contents/EmailLoginContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Button } from '@/app/components/ui/button'
import { AppOrbitImage } from '@/app/components/ui/app-orbit-image'
import { isValidEmail, isStrongPassword } from '@/app/utils/utils'
import { useAuth } from '@/app/components/auth/useAuth'
import ResetPassword from './ResetPassword'

type Props = {
initialEmail?: string
Expand All @@ -14,6 +15,7 @@ export default function EmailLoginContent({
initialEmail = '',
onBack,
}: Props) {
const [showResetPassword, setShowResetPassword] = useState(false)
const [email, setEmail] = useState(initialEmail)
const [password, setPassword] = useState('')

Expand Down Expand Up @@ -43,6 +45,15 @@ export default function EmailLoginContent({
}
}

if (showResetPassword) {
return (
<ResetPassword
email={emailOk ? email : undefined}
onBack={() => setShowResetPassword(false)}
/>
)
}

return (
<div className="flex h-full w-full bg-background">
{/* Left: form */}
Expand Down Expand Up @@ -115,9 +126,12 @@ export default function EmailLoginContent({
<button className="hover:underline" onClick={onBack}>
Log in with a different email
</button>
<span className="hover:underline cursor-default">
<button
className="hover:underline"
onClick={() => setShowResetPassword(true)}
>
Forgot password?
</span>
</button>
</div>
</div>
</div>
Expand Down
226 changes: 226 additions & 0 deletions app/components/welcome/contents/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { useMemo, useState } from 'react'
import { Button } from '@/app/components/ui/button'
import { AppOrbitImage } from '../../ui/app-orbit-image'
import { STORE_KEYS } from '../../../../lib/constants/store-keys'
import { isValidEmail } from '@/app/utils/utils'

type Props = {
email?: string
onBack: () => void
}

export default function ResetPassword({ email, onBack }: Props) {
const storedUser = window.electron?.store?.get(STORE_KEYS.AUTH)?.user
const initialEmail = email || storedUser?.email || ''

const [editableEmail, setEditableEmail] = useState(initialEmail)
const [isEditingEmail, setIsEditingEmail] = useState(!initialEmail)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [emailSent, setEmailSent] = useState(false)
const [seconds, setSeconds] = useState(0)
const [isResending, setIsResending] = useState(false)

const emailOk = useMemo(() => isValidEmail(editableEmail), [editableEmail])

const handleContinue = async () => {
if (!editableEmail || !emailOk) {
setError('Please enter a valid email address')
return
}

try {
setIsLoading(true)
setError(null)

const res = await window.api.invoke('auth0-reset-password', {
email: editableEmail,
})

if (res?.success) {
setEmailSent(true)
setSeconds(30)
// Start countdown
const id = setInterval(() => {
setSeconds(s => {
if (s <= 1) {
clearInterval(id)
return 0
}
return s - 1
})
}, 1000)
} else {
setError(res?.error || 'Failed to send reset email')
}
} catch (e: any) {
setError(e?.message || 'An error occurred')
} finally {
setIsLoading(false)
}
}

const handleResend = async () => {
if (seconds > 0 || isResending) return

try {
setIsResending(true)
setError(null)

const res = await window.api.invoke('auth0-reset-password', {
email: editableEmail,
})

if (res?.success) {
setSeconds(30)
const id = setInterval(() => {
setSeconds(s => {
if (s <= 1) {
clearInterval(id)
return 0
}
return s - 1
})
}, 1000)
} else {
setError(res?.error || 'Failed to resend reset email')
}
} catch (e: any) {
setError(e?.message || 'An error occurred')
} finally {
setIsResending(false)
}
}

const handleOpenEmailApp = () => {
window.api.invoke('web-open-url', 'mailto:')
}

// Check Your Inbox view (after email sent)
if (emailSent) {
return (
<div className="flex h-full w-full bg-background">
{/* Left content */}
<div className="flex w-1/2 flex-col justify-center px-16">
<button
onClick={onBack}
className="mb-6 w-fit text-sm text-muted-foreground hover:underline"
>
Back
</button>

<div className="mb-8">
<h1 className="text-3xl font-semibold text-foreground">
Check Your Inbox
</h1>
<p className="mt-2 text-sm text-muted-foreground">
We've sent a reset link to {editableEmail}.
</p>
<p className="mt-4 text-sm text-muted-foreground">
Follow the instructions in the email to set a new password.
</p>
</div>

<Button
onClick={handleOpenEmailApp}
className="h-10 w-full justify-center"
>
Open email app
</Button>

<button
className="mt-4 text-sm text-foreground"
onClick={handleResend}
disabled={seconds > 0 || isResending}
>
Didn't get the email?{' '}
<span className="underline">
{seconds > 0
? `Resend (${seconds}s)`
: isResending
? 'Resending…'
: 'Resend'}
</span>
</button>

{error && (
<p className="mt-2 text-center text-xs text-destructive">{error}</p>
)}
</div>

{/* Right illustration */}
<div className="flex w-1/2 items-center justify-center border-l border-border bg-muted/20">
<AppOrbitImage />
</div>
</div>
)
}

// Initial Reset Password view
return (
<div className="flex h-full w-full bg-background">
{/* Left content */}
<div className="flex w-1/2 flex-col justify-center px-16">
<button
onClick={onBack}
className="mb-6 w-fit text-sm text-muted-foreground hover:underline"
>
Back
</button>

<div className="mb-8">
<h1 className="text-3xl font-semibold text-foreground">
Reset Password
</h1>
{isEditingEmail ? (
<div className="mt-4">
<label className="text-sm text-muted-foreground">
Enter your email to receive a reset link
</label>
<input
type="email"
placeholder="Enter your email"
value={editableEmail}
onChange={e => setEditableEmail(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && emailOk) {
e.preventDefault()
handleContinue()
}
}}
className="mt-2 h-10 w-full rounded-md border border-border bg-background px-3 text-foreground placeholder:text-muted-foreground"
/>
</div>
) : (
<p className="mt-2 text-sm text-muted-foreground">
We'll send a reset link to {editableEmail}.{' '}
<button
onClick={() => setIsEditingEmail(true)}
className="underline hover:text-foreground"
>
Change
</button>
</p>
)}
</div>

<Button
onClick={handleContinue}
disabled={isLoading || !emailOk}
className="h-10 w-full justify-center"
>
{isLoading ? 'Sending…' : 'Continue'}
</Button>

{error && (
<p className="mt-2 text-center text-xs text-destructive">{error}</p>
)}
</div>

{/* Right illustration */}
<div className="flex w-1/2 items-center justify-center border-l border-border bg-muted/20">
<AppOrbitImage />
</div>
</div>
)
}
29 changes: 22 additions & 7 deletions app/components/welcome/contents/SignInContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useDictionaryStore } from '@/app/store/useDictionaryStore'
import { AppOrbitImage } from '@/app/components/ui/app-orbit-image'
import { STORE_KEYS } from '../../../../lib/constants/store-keys'
import { isValidEmail, isStrongPassword } from '@/app/utils/utils'
import ResetPassword from './ResetPassword'

// Auth provider configuration
const AUTH_PROVIDERS = {
Expand Down Expand Up @@ -120,6 +121,7 @@ export default function SignInContent() {
const [password, setPassword] = useState('')
const [isLoggingIn, setIsLoggingIn] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [showResetPassword, setShowResetPassword] = useState(false)

const {
user,
Expand Down Expand Up @@ -412,10 +414,14 @@ export default function SignInContent() {
return renderSingleProviderOption(userProvider)
}

if (showResetPassword) {
return <ResetPassword onBack={() => setShowResetPassword(false)} />
}

return (
<div className="flex h-full w-full bg-background">
{/* Left side - Sign in form */}
<div className="flex w-[30%] flex-col items-center justify-center px-12 py-12">
<div className="flex w-[50%] flex-col items-center justify-center px-12 py-12">
<div className="w-full max-w-md">
{/* Logo */}
<div className="mb-8 bg-black rounded-md p-2 w-10 h-10 mx-auto">
Expand Down Expand Up @@ -454,11 +460,8 @@ export default function SignInContent() {
)}

{/* Link to create new account */}
<div className="text-center mt-8">
<div className="text-center mt-3 flex justify-between">
<p className="text-sm text-muted-foreground">
{userProvider
? 'Sign in with a different account?'
: 'Need to create an account?'}{' '}
<button
onClick={() => {
// Clear auth to allow selecting a different account, but do not reset onboarding
Expand All @@ -469,15 +472,27 @@ export default function SignInContent() {
}}
className="text-foreground underline font-medium"
>
{userProvider ? 'Switch account' : 'Create account'}
{userProvider
? 'Log in with a different email'
: 'Create account'}
</button>
</p>
{(!userProvider || userProvider === 'email') && (
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the page that shows when a user is already signed in and has logged out / timed out. we only show the forgot password here if they are provided via email rather than something like google or apple accounts

<p className="text-sm text-muted-foreground">
<button
onClick={() => setShowResetPassword(true)}
className="text-foreground underline font-medium"
>
Forgot password
</button>
</p>
)}
</div>
</div>
</div>

{/* Right side - Placeholder for image */}
<div className="flex w-[70%] bg-muted/20 items-center justify-center border-l border-border">
<div className="flex w-[50%] bg-muted/20 items-center justify-center border-l border-border">
<AppOrbitImage />
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions lib/window/ipcEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,15 @@ export function registerIPC() {
})
})

// Send password reset email via server proxy
handleIPC('auth0-reset-password', async (_e, { email }) => {
if (!email) return { success: false, error: 'Missing email' }
return itoHttpClient.post('/auth0/reset-password', {
email,
connection: Auth0Connections.database,
})
})

// Check if email exists for db signup and whether it's verified (via server proxy)
handleIPC('auth0-check-email', async (_e, { email }) => {
if (!email) return { success: false, error: 'Missing email' }
Expand Down
Loading