jira/apps/space/components/account/auth-forms/unique-code.tsx
Anmol Singh Bhatia b7281eacfe
refactor: migrate space icons to @makeplane/propel (#9729)
* refactor: migrate space icons to @makeplane/propel

Replace confirmed icon symbols in apps/space with their audited
@makeplane/propel/icons targets. Unresolved symbols (StateGroupIcon,
PriorityIcon, PlaneLockup, PlaneLogo, Circle, CircleDashed, SignalHigh)
stay on their existing imports.

Propel icons paint with fill rather than stroke, so stroke-placeholder
classes become text-placeholder to preserve the rendered color. Lucide
size props become explicit width/height, and one chevron gains an
explicit height because Propel defaults to 1em where the legacy
IconWrapper defaulted to 16.

CycleGroupIcon is deliberately not migrated: it dispatches a different
glyph and color per cycle status, which CyclesOutline cannot express.

* fix: address icon migration review on space

Drop strokeWidth from Propel icons. The generated glyphs paint with
fill="currentColor", so the prop reached the svg and changed nothing;
keeping it implied a control that no longer exists.

Wrap the password and email-clear icon click targets in buttons. These
already carried onClick directly on the icon, but Propel bakes
aria-hidden="true" onto the svg, so the migration also hid them from
assistive tech. The buttons restore an accessible name and put the
controls in the tab order, matching the pattern email.tsx already uses.
2026-09-02 20:36:06 +05:30

162 lines
5.8 KiB
TypeScript

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useEffect, useState } from "react";
import { CloseCircleOutline, TickCircleOutline } from "@makeplane/propel/icons";
// plane imports
import { API_BASE_URL } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Input, Spinner } from "@plane/ui";
// hooks
import useTimer from "@/hooks/use-timer";
// types
import { EAuthModes } from "@/types/auth";
// services
const authService = new AuthService();
type TAuthUniqueCodeForm = {
mode: EAuthModes;
email: string;
nextPath: string | undefined;
handleEmailClear: () => void;
generateEmailUniqueCode: (email: string) => Promise<{ code: string } | undefined>;
};
type TUniqueCodeFormValues = {
email: string;
code: string;
};
const defaultValues: TUniqueCodeFormValues = {
email: "",
code: "",
};
export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
// derived values
const defaultResetTimerValue = 5;
// states
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isSubmitting, setIsSubmitting] = useState(false);
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));
const generateNewCode = async (email: string) => {
try {
setIsRequestingNewCode(true);
const uniqueCode = await generateEmailUniqueCode(email);
setResendCodeTimer(defaultResetTimerValue);
handleFormChange("code", uniqueCode?.code || "");
setIsRequestingNewCode(false);
} catch {
setResendCodeTimer(0);
console.error("Error while requesting new code");
setIsRequestingNewCode(false);
}
};
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
return (
<form
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
<input type="hidden" value={nextPath} name="next_path" />
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
Email
</label>
<div className={`relative flex items-center rounded-md border border-subtle bg-surface-1`}>
<Input
id="email"
name="email"
type="email"
value={uniqueCodeFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
placeholder="name@company.com"
className={`h-10 w-full border-0 disable-autofill-style placeholder:text-placeholder`}
autoComplete="off"
disabled
/>
{uniqueCodeFormData.email.length > 0 && (
<button
type="button"
aria-label="Clear email"
className="absolute right-3 hover:cursor-pointer"
onClick={handleEmailClear}
tabIndex={-1}
>
<CloseCircleOutline className="h-5 w-5 text-placeholder" />
</button>
)}
</div>
</div>
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="code">
Unique code
</label>
<Input
name="code"
value={uniqueCodeFormData.code}
onChange={(e) => handleFormChange("code", e.target.value)}
placeholder="123456"
className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder"
autoComplete="off"
autoFocus
/>
<div className="flex w-full items-center justify-between px-1 pt-1 text-11">
<p className="flex items-center gap-1 font-medium text-success-primary">
<TickCircleOutline height={12} width={12} />
Paste the code sent to your email
</p>
<button
type="button"
onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={`${
isRequestNewCodeDisabled
? "text-placeholder"
: "font-medium text-accent-secondary hover:text-accent-secondary"
}`}
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
? `Resend in ${resendTimerCode}s`
: isRequestingNewCode
? "Requesting new code"
: "Resend"}
</button>
</div>
</div>
<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={isButtonDisabled}>
{isRequestingNewCode ? "Sending code" : isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</div>
</form>
);
}