Onboarding improvements
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m14s
Build and Push Lunchtime Images (Kaniko) / build-and-push (push) Successful in 1m14s
This commit is contained in:
@@ -22,6 +22,7 @@ import WelcomeOnboardingModal from "./components/modals/WelcomeOnboardingModal";
|
|||||||
import { navigateTo, parseRoute, subscribeToRouteChange } from "./lib/routing";
|
import { navigateTo, parseRoute, subscribeToRouteChange } from "./lib/routing";
|
||||||
import { apiService } from "./lib/services";
|
import { apiService } from "./lib/services";
|
||||||
import {
|
import {
|
||||||
|
clearUserId,
|
||||||
ensureUserId,
|
ensureUserId,
|
||||||
hasStoredUserId,
|
hasStoredUserId,
|
||||||
updateUserId,
|
updateUserId,
|
||||||
@@ -163,6 +164,7 @@ function AppContent({
|
|||||||
userId,
|
userId,
|
||||||
userEmail,
|
userEmail,
|
||||||
onUserEmailChange,
|
onUserEmailChange,
|
||||||
|
onLogout,
|
||||||
}: {
|
}: {
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
setThemeMode: (mode: ThemeMode) => void;
|
setThemeMode: (mode: ThemeMode) => void;
|
||||||
@@ -171,6 +173,7 @@ function AppContent({
|
|||||||
userId: string;
|
userId: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
||||||
|
onLogout: () => void;
|
||||||
}) {
|
}) {
|
||||||
const appBackground =
|
const appBackground =
|
||||||
themeMode === "dark"
|
themeMode === "dark"
|
||||||
@@ -200,6 +203,7 @@ function AppContent({
|
|||||||
}}
|
}}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
onUserEmailChange={onUserEmailChange}
|
onUserEmailChange={onUserEmailChange}
|
||||||
|
onLogout={onLogout}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MemoAnnouncements announcements={announcements} />
|
<MemoAnnouncements announcements={announcements} />
|
||||||
@@ -371,6 +375,15 @@ export default function App() {
|
|||||||
return userEmail;
|
return userEmail;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
clearUserId();
|
||||||
|
setUserId("");
|
||||||
|
setUserEmail("");
|
||||||
|
setIsOnboardingOpen(true);
|
||||||
|
navigateTo("/");
|
||||||
|
message.success("Logged out");
|
||||||
|
};
|
||||||
|
|
||||||
const completeCreateAccount = async (email: string) => {
|
const completeCreateAccount = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
await apiService.account.register(email);
|
await apiService.account.register(email);
|
||||||
@@ -406,6 +419,7 @@ export default function App() {
|
|||||||
userId={userId}
|
userId={userId}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
onUserEmailChange={handleUserEmailChange}
|
onUserEmailChange={handleUserEmailChange}
|
||||||
|
onLogout={handleLogout}
|
||||||
/>
|
/>
|
||||||
<WelcomeOnboardingModal
|
<WelcomeOnboardingModal
|
||||||
open={isOnboardingOpen}
|
open={isOnboardingOpen}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, Flex, Form, Input, Modal, Space, Typography } from "antd";
|
import { Alert, Button, Flex, Form, Input, Modal, Space, Typography } from "antd";
|
||||||
import ThemeModeToggle from "./utils/ThemeModeToggle";
|
import ThemeModeToggle from "./utils/ThemeModeToggle";
|
||||||
|
|
||||||
type ThemeMode = "light" | "dark";
|
type ThemeMode = "light" | "dark";
|
||||||
@@ -23,6 +23,7 @@ export default function WelcomeOnboardingModal({
|
|||||||
const [form] = Form.useForm<{ email: string }>();
|
const [form] = Form.useForm<{ email: string }>();
|
||||||
const [lookupState, setLookupState] = useState<EmailLookupState>("idle");
|
const [lookupState, setLookupState] = useState<EmailLookupState>("idle");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
const lookupTimerRef = useRef<number | null>(null);
|
const lookupTimerRef = useRef<number | null>(null);
|
||||||
const lookupRequestIdRef = useRef(0);
|
const lookupRequestIdRef = useRef(0);
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export default function WelcomeOnboardingModal({
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLookupState("idle");
|
setLookupState("idle");
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
setHasSentEmail(false);
|
||||||
lookupRequestIdRef.current += 1;
|
lookupRequestIdRef.current += 1;
|
||||||
if (lookupTimerRef.current) {
|
if (lookupTimerRef.current) {
|
||||||
window.clearTimeout(lookupTimerRef.current);
|
window.clearTimeout(lookupTimerRef.current);
|
||||||
@@ -80,6 +82,9 @@ export default function WelcomeOnboardingModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const buttonLabel = useMemo(() => {
|
const buttonLabel = useMemo(() => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return "Email sent";
|
||||||
|
}
|
||||||
if (lookupState === "exists") {
|
if (lookupState === "exists") {
|
||||||
return "Migrate account";
|
return "Migrate account";
|
||||||
}
|
}
|
||||||
@@ -90,14 +95,16 @@ export default function WelcomeOnboardingModal({
|
|||||||
return "Create new account";
|
return "Create new account";
|
||||||
}
|
}
|
||||||
return "Continue";
|
return "Continue";
|
||||||
}, [lookupState]);
|
}, [hasSentEmail, lookupState]);
|
||||||
|
|
||||||
|
const isLookupResolved = lookupState === "exists" || lookupState === "new";
|
||||||
|
|
||||||
const helperText = useMemo(() => {
|
const helperText = useMemo(() => {
|
||||||
if (lookupState === "exists") {
|
if (lookupState === "exists") {
|
||||||
return "An account was found for this email. We'll send a migration confirmation link.";
|
return "Log in using this email.";
|
||||||
}
|
}
|
||||||
if (lookupState === "new") {
|
if (lookupState === "new") {
|
||||||
return "No account found for this email. We'll create a new account.";
|
return "Register using this email.";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}, [lookupState]);
|
}, [lookupState]);
|
||||||
@@ -127,6 +134,10 @@ export default function WelcomeOnboardingModal({
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onValuesChange={(_changedValues, values) => {
|
onValuesChange={(_changedValues, values) => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentEmail = String(values.email || "");
|
const currentEmail = String(values.email || "");
|
||||||
if (lookupTimerRef.current) {
|
if (lookupTimerRef.current) {
|
||||||
window.clearTimeout(lookupTimerRef.current);
|
window.clearTimeout(lookupTimerRef.current);
|
||||||
@@ -138,6 +149,10 @@ export default function WelcomeOnboardingModal({
|
|||||||
}, 350);
|
}, 350);
|
||||||
}}
|
}}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedEmail = normalizeEmail(values.email);
|
const normalizedEmail = normalizeEmail(values.email);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -152,6 +167,8 @@ export default function WelcomeOnboardingModal({
|
|||||||
} else {
|
} else {
|
||||||
await onCreateAccount(normalizedEmail);
|
await onCreateAccount(normalizedEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHasSentEmail(true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -166,14 +183,41 @@ export default function WelcomeOnboardingModal({
|
|||||||
]}
|
]}
|
||||||
extra={helperText || undefined}
|
extra={helperText || undefined}
|
||||||
>
|
>
|
||||||
<Input placeholder="alex@example.com" autoFocus maxLength={320} />
|
<Input
|
||||||
|
placeholder="alex@example.com"
|
||||||
|
autoFocus
|
||||||
|
maxLength={320}
|
||||||
|
disabled={hasSentEmail}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{hasSentEmail ? (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
description="Email sent! Please check your inbox and spam folder for the confirmation email."
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{hasSentEmail ? (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
setHasSentEmail(false);
|
||||||
|
setLookupState("idle");
|
||||||
|
}}
|
||||||
|
style={{ paddingInline: 0 }}
|
||||||
|
>
|
||||||
|
Change email
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
block
|
block
|
||||||
loading={isSubmitting || lookupState === "checking"}
|
loading={!hasSentEmail && (isSubmitting || lookupState === "checking")}
|
||||||
|
disabled={hasSentEmail || !isLookupResolved}
|
||||||
>
|
>
|
||||||
{buttonLabel}
|
{buttonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { CheckOutlined } from "@ant-design/icons";
|
import { CheckOutlined, LogoutOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
Popconfirm,
|
||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
@@ -14,9 +15,11 @@ import {
|
|||||||
export default function AccountSettingsPopoverContent({
|
export default function AccountSettingsPopoverContent({
|
||||||
userEmail,
|
userEmail,
|
||||||
onUserEmailChange,
|
onUserEmailChange,
|
||||||
|
onLogout,
|
||||||
}: {
|
}: {
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
||||||
|
onLogout: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [form] = Form.useForm<{ email: string }>();
|
const [form] = Form.useForm<{ email: string }>();
|
||||||
const watchedEmail = Form.useWatch("email", form) || "";
|
const watchedEmail = Form.useWatch("email", form) || "";
|
||||||
@@ -98,6 +101,25 @@ export default function AccountSettingsPopoverContent({
|
|||||||
Discard
|
Discard
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<Popconfirm
|
||||||
|
title="Log out?"
|
||||||
|
description="This clears your local user token on this device."
|
||||||
|
okText="Log out"
|
||||||
|
cancelText="Cancel"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
onConfirm={onLogout}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
icon={<LogoutOutlined />}
|
||||||
|
danger
|
||||||
|
block
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
</Form>
|
</Form>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ export default function TopNav({
|
|||||||
onHome,
|
onHome,
|
||||||
userEmail,
|
userEmail,
|
||||||
onUserEmailChange,
|
onUserEmailChange,
|
||||||
|
onLogout,
|
||||||
}: {
|
}: {
|
||||||
themeMode: "light" | "dark";
|
themeMode: "light" | "dark";
|
||||||
onThemeChange: (mode: "light" | "dark") => void;
|
onThemeChange: (mode: "light" | "dark") => void;
|
||||||
onHome: () => void;
|
onHome: () => void;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
onUserEmailChange: (nextUserEmail: string) => void | Promise<string>;
|
||||||
|
onLogout: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
|
|
||||||
@@ -50,6 +52,10 @@ export default function TopNav({
|
|||||||
<AccountSettingsPopoverContent
|
<AccountSettingsPopoverContent
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
onUserEmailChange={onUserEmailChange}
|
onUserEmailChange={onUserEmailChange}
|
||||||
|
onLogout={() => {
|
||||||
|
setIsPopoverOpen(false);
|
||||||
|
onLogout();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, Flex, Form, Input, Modal, Space, Typography } from "antd";
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
} from "antd";
|
||||||
import ThemeModeToggle from "../utils/ThemeModeToggle";
|
import ThemeModeToggle from "../utils/ThemeModeToggle";
|
||||||
|
|
||||||
type ThemeMode = "light" | "dark";
|
type ThemeMode = "light" | "dark";
|
||||||
@@ -23,6 +32,7 @@ export default function WelcomeOnboardingModal({
|
|||||||
const [form] = Form.useForm<{ email: string }>();
|
const [form] = Form.useForm<{ email: string }>();
|
||||||
const [lookupState, setLookupState] = useState<EmailLookupState>("idle");
|
const [lookupState, setLookupState] = useState<EmailLookupState>("idle");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
const lookupTimerRef = useRef<number | null>(null);
|
const lookupTimerRef = useRef<number | null>(null);
|
||||||
const lookupRequestIdRef = useRef(0);
|
const lookupRequestIdRef = useRef(0);
|
||||||
|
|
||||||
@@ -62,6 +72,7 @@ export default function WelcomeOnboardingModal({
|
|||||||
form.resetFields();
|
form.resetFields();
|
||||||
setLookupState("idle");
|
setLookupState("idle");
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
setHasSentEmail(false);
|
||||||
lookupRequestIdRef.current += 1;
|
lookupRequestIdRef.current += 1;
|
||||||
if (lookupTimerRef.current) {
|
if (lookupTimerRef.current) {
|
||||||
window.clearTimeout(lookupTimerRef.current);
|
window.clearTimeout(lookupTimerRef.current);
|
||||||
@@ -80,27 +91,22 @@ export default function WelcomeOnboardingModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const buttonLabel = useMemo(() => {
|
const buttonLabel = useMemo(() => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return "Email sent";
|
||||||
|
}
|
||||||
if (lookupState === "exists") {
|
if (lookupState === "exists") {
|
||||||
return "Migrate account";
|
return "Log In";
|
||||||
}
|
}
|
||||||
if (lookupState === "checking") {
|
if (lookupState === "checking") {
|
||||||
return "Checking account...";
|
return "Checking account...";
|
||||||
}
|
}
|
||||||
if (lookupState === "new") {
|
if (lookupState === "new") {
|
||||||
return "Create new account";
|
return "Register";
|
||||||
}
|
}
|
||||||
return "Continue";
|
return "Continue";
|
||||||
}, [lookupState]);
|
}, [hasSentEmail, lookupState]);
|
||||||
|
|
||||||
const helperText = useMemo(() => {
|
const isLookupResolved = lookupState === "exists" || lookupState === "new";
|
||||||
if (lookupState === "exists") {
|
|
||||||
return "An account was found for this email. We'll send a migration confirmation link.";
|
|
||||||
}
|
|
||||||
if (lookupState === "new") {
|
|
||||||
return "No account found for this email. We'll create a new account.";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}, [lookupState]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -116,17 +122,26 @@ export default function WelcomeOnboardingModal({
|
|||||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
Welcome to Lunchtime
|
Welcome to Lunchtime
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<ThemeModeToggle themeMode={themeMode} onThemeChange={onThemeChange} />
|
<ThemeModeToggle
|
||||||
|
themeMode={themeMode}
|
||||||
|
onThemeChange={onThemeChange}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Typography.Paragraph style={{ marginBottom: 6 }}>
|
<Typography.Paragraph style={{ marginBottom: 6 }}>
|
||||||
Enter your account email to continue. We'll automatically detect whether to create a new account or migrate an existing one.
|
Enter your account email to continue. We'll send you a confirmation
|
||||||
|
link to log in or create an account.
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="horizontal"
|
||||||
|
requiredMark={false}
|
||||||
onValuesChange={(_changedValues, values) => {
|
onValuesChange={(_changedValues, values) => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentEmail = String(values.email || "");
|
const currentEmail = String(values.email || "");
|
||||||
if (lookupTimerRef.current) {
|
if (lookupTimerRef.current) {
|
||||||
window.clearTimeout(lookupTimerRef.current);
|
window.clearTimeout(lookupTimerRef.current);
|
||||||
@@ -138,6 +153,10 @@ export default function WelcomeOnboardingModal({
|
|||||||
}, 350);
|
}, 350);
|
||||||
}}
|
}}
|
||||||
onFinish={async (values) => {
|
onFinish={async (values) => {
|
||||||
|
if (hasSentEmail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedEmail = normalizeEmail(values.email);
|
const normalizedEmail = normalizeEmail(values.email);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -152,31 +171,62 @@ export default function WelcomeOnboardingModal({
|
|||||||
} else {
|
} else {
|
||||||
await onCreateAccount(normalizedEmail);
|
await onCreateAccount(normalizedEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHasSentEmail(true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Space direction="vertical" size={12} style={{ width: "100%" }}>
|
||||||
label="Email"
|
<Form.Item
|
||||||
name="email"
|
label="Email"
|
||||||
rules={[
|
name="email"
|
||||||
{ required: true, message: "Email cannot be empty." },
|
rules={[
|
||||||
{ type: "email", message: "Enter a valid email address." },
|
{ required: true, message: "Email cannot be empty." },
|
||||||
]}
|
{ type: "email", message: "Enter a valid email address." },
|
||||||
extra={helperText || undefined}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="alex@example.com" autoFocus maxLength={320} />
|
<Input
|
||||||
</Form.Item>
|
placeholder="alex@example.com"
|
||||||
|
autoFocus
|
||||||
|
maxLength={320}
|
||||||
|
disabled={hasSentEmail}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Button
|
{hasSentEmail ? (
|
||||||
type="primary"
|
<Alert
|
||||||
htmlType="submit"
|
type="info"
|
||||||
block
|
showIcon
|
||||||
loading={isSubmitting || lookupState === "checking"}
|
description="Email sent! Please check your inbox and spam folder for the confirmation email."
|
||||||
>
|
/>
|
||||||
{buttonLabel}
|
) : null}
|
||||||
</Button>
|
|
||||||
|
{hasSentEmail ? (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => {
|
||||||
|
setHasSentEmail(false);
|
||||||
|
setLookupState("idle");
|
||||||
|
}}
|
||||||
|
style={{ paddingInline: 0 }}
|
||||||
|
>
|
||||||
|
Change email
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
loading={
|
||||||
|
!hasSentEmail && (isSubmitting || lookupState === "checking")
|
||||||
|
}
|
||||||
|
disabled={hasSentEmail || !isLookupResolved}
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { USER_TOKEN_KEY } from "./constants";
|
import { USER_TOKEN_KEY } from "./constants";
|
||||||
import { getStoredValue, setStoredValue } from "./storage";
|
import { getStoredValue, removeStoredValue, setStoredValue } from "./storage";
|
||||||
|
|
||||||
const emailByUserId = new Map<string, string>();
|
const emailByUserId = new Map<string, string>();
|
||||||
|
|
||||||
@@ -72,3 +72,7 @@ export function updateUserEmail(nextUserEmail: string, userId: string) {
|
|||||||
export function regenerateUserId() {
|
export function regenerateUserId() {
|
||||||
return createUserId();
|
return createUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearUserId() {
|
||||||
|
removeStoredValue(USER_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import AsyncContent from "../components/utils/AsyncContent";
|
|||||||
import ReadOnlyOrderOverviewCard from "../components/orders/ReadOnlyOrderOverviewCard";
|
import ReadOnlyOrderOverviewCard from "../components/orders/ReadOnlyOrderOverviewCard";
|
||||||
import { useApiRequest } from "../hooks/useApiRequest";
|
import { useApiRequest } from "../hooks/useApiRequest";
|
||||||
import { apiService } from "../lib/services";
|
import { apiService } from "../lib/services";
|
||||||
import { navigateTo } from "../lib/routing";
|
import { navigateTo, replaceRoute } from "../lib/routing";
|
||||||
import type {
|
import type {
|
||||||
|
ApiError,
|
||||||
GetAdminViewResponse,
|
GetAdminViewResponse,
|
||||||
Order,
|
Order,
|
||||||
OrderFormConfig,
|
OrderFormConfig,
|
||||||
@@ -25,6 +26,14 @@ type AdminViewData = Order & {
|
|||||||
config: OrderFormConfig;
|
config: OrderFormConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function shouldRedirectToParticipant(error: unknown): boolean {
|
||||||
|
const apiError = error as Partial<ApiError> | null;
|
||||||
|
const status = apiError?.status;
|
||||||
|
const message = error instanceof Error ? error.message : "";
|
||||||
|
|
||||||
|
return status === 404 && message === "Admin order view not found";
|
||||||
|
}
|
||||||
|
|
||||||
function getEstimatedTotalNumber(rawValue: unknown): number | null {
|
function getEstimatedTotalNumber(rawValue: unknown): number | null {
|
||||||
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
||||||
return rawValue;
|
return rawValue;
|
||||||
@@ -100,6 +109,11 @@ export default function AdminView({ orderId }: { orderId: string }) {
|
|||||||
setSelectedRowKeys([]);
|
setSelectedRowKeys([]);
|
||||||
},
|
},
|
||||||
onError: (requestError) => {
|
onError: (requestError) => {
|
||||||
|
if (shouldRedirectToParticipant(requestError)) {
|
||||||
|
replaceRoute(`/order/${orderId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
message.error(requestError?.message || "Admin view could not be loaded.");
|
message.error(requestError?.message || "Admin view could not be loaded.");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user