Improve authentication callback UI and error handling

- Enhanced callback.astro with loading states and user feedback
- Fixed React prop naming in HeaderNavigation (stroke-width -> strokeWidth)
- Added safe utility for promise error handling
- Improved TicketForm authentication check using safe wrapper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Clas Wen 2025-10-15 20:13:59 +08:00
parent bfa15f2a42
commit a2eb15c002
4 changed files with 142 additions and 10 deletions

View file

@ -65,8 +65,8 @@ export default function App() {
</span>
{
item.target == "_blank" && (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" className="size-4 ml-0.5 inline-block">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-4 ml-0.5 inline-block">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
)
}
@ -103,8 +103,8 @@ export default function App() {
</span>
{
item.target == "_blank" && (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" className="size-5 ml-1 inline-block">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-5 ml-1 inline-block">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
)
}

View file

@ -1,21 +1,134 @@
<div></div>
---
import BaseLayout from "../layouts/BaseLayout.astro"
import RepairHeader from "../components/header/RepairHeader.astro"
---
<BaseLayout primaryTitle="Authentication Callback">
<RepairHeader></RepairHeader>
<section class="min-h-[70vh]">
<div class="section-content my-16 flex flex-col gap-8">
<div>
<div>
<div id="status-text" class="text-xl font-bold">处理登录中...</div>
<div id="status-subtitle" class="text-sm text-gray-700">
正在验证身份
</div>
</div>
<div class="mt-4">
<div id="loading-spinner" class="flex justify-center my-8">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
<div id="status-message" class="text-gray-700"></div>
</div>
</div>
<div id="continue-button-wrapper" class="mt-4 hidden">
<button
id="continue-button"
class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors font-medium"
>
继续
</button>
</div>
</div>
</section>
</BaseLayout>
<style>
.text-success {
color: #10b981;
}
.text-error {
color: #ef4444;
}
</style>
<script>
import { makeLogtoClient } from "../utils/auth"
const statusText = document.getElementById("status-text")
const statusSubtitle = document.getElementById("status-subtitle")
const statusMessage = document.getElementById("status-message")
const continueButtonWrapper = document.getElementById("continue-button-wrapper")
const continueButton = document.getElementById("continue-button")
const loadingSpinner = document.getElementById("loading-spinner")
const updateStatus = (title, subtitle, message, isError = false) => {
statusText.textContent = title
statusSubtitle.textContent = subtitle
statusMessage.textContent = message
if (isError) {
statusText.classList.add("text-error")
statusText.classList.remove("text-success")
}
else {
statusText.classList.add("text-success")
statusText.classList.remove("text-error")
}
}
const showContinueButton = () => {
loadingSpinner.classList.add("hidden")
continueButtonWrapper.classList.remove("hidden")
}
const redirectToHome = () => {
window.location.assign("/")
}
continueButton.addEventListener("click", redirectToHome)
// Show manual continue button after 5 seconds as fallback
const fallbackTimer = setTimeout(() => {
updateStatus(
"等待时间较长...",
"如果未自动跳转,请手动继续",
"你可以点击下方按钮手动继续。",
false,
)
showContinueButton()
}, 5000)
const callbackHandler = async (logtoClient) => {
console.log("callbackHandler")
try {
await logtoClient.handleSignInCallback(window.location.href)
if (!logtoClient.isAuthenticated) {
console.log("User not authenticated")
window.location.assign("/")
clearTimeout(fallbackTimer)
updateStatus(
"认证失败",
"无法验证你的登录",
"无法验证你的登录信息。正在跳转到首页...",
true,
)
showContinueButton()
setTimeout(redirectToHome, 2000)
return
}
// Authentication successful
clearTimeout(fallbackTimer)
updateStatus(
"登录成功!",
"验证完成",
"正在跳转到首页...",
)
showContinueButton()
setTimeout(redirectToHome, 1500)
}
catch (error) {
console.log(error)
window.location.assign("/")
clearTimeout(fallbackTimer)
updateStatus(
"认证错误",
"登录过程中出现错误",
`登录过程中出现错误:${error.message || "未知错误"}。点击下方按钮返回首页。`,
true,
)
showContinueButton()
}
}
@ -26,5 +139,13 @@ try {
}
catch (error) {
console.log(error)
clearTimeout(fallbackTimer)
updateStatus(
"初始化错误",
"无法初始化认证客户端",
"无法初始化认证客户端。点击下方按钮返回首页。",
true,
)
showContinueButton()
}
</script>

View file

@ -3,6 +3,7 @@ import { makeLogtoClient } from "../../utils/auth"
import type { UserInfoResponse } from "@logto/browser"
import { Alert, Form, Input, Button, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import { safe } from "../../utils/safe"
type TicketFormData = {
model?: string
@ -275,12 +276,11 @@ export default function App() {
const check = async () => {
const createRepairPath = "/repair/create-ticket"
try {
const authenticated = await makeLogtoClient().isAuthenticated()
if (!authenticated) {
const [res, err] = await safe(makeLogtoClient().getIdTokenClaims())
if (err) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
const res = await makeLogtoClient().getIdTokenClaims()
setUserInfo(res)
}
catch (error) {

11
src/utils/safe.ts Normal file
View file

@ -0,0 +1,11 @@
export async function safe<T>(
promise: Promise<T> | (() => T),
): Promise<[T | null, Error | null]> {
try {
const result = promise instanceof Promise ? await promise : promise()
return [result, null]
}
catch (err) {
return [null, err instanceof Error ? err : new Error(String(err))]
}
}