Skip to content

WD-33303 add countdown for resend code in verification flow#847

Open
abhigyanghosh30 wants to merge 16 commits intomainfrom
WD-33303-add-timeout
Open

WD-33303 add countdown for resend code in verification flow#847
abhigyanghosh30 wants to merge 16 commits intomainfrom
WD-33303-add-timeout

Conversation

@abhigyanghosh30
Copy link

Done:

  • Added a timeout to the resend code button
  • Also added a few error notifications

QA:
Setup the repo according to the doc: https://docs.google.com/document/d/15PrLDpaERf7FmyHwZl4GlFLPIxaX2zF2rKENxCcKgNU/edit?tab=t.0

  • Visit /ui/verification
  • Enter your email
  • Enter a fake code
    • Should show an error
  • Try to resend code
    • Should resend the code
    • Should disable the link with a 10s timer
  • Visit localhost:4436 to get the correct code
  • Enter the correct code
    • Should succeed

@abhigyanghosh30 abhigyanghosh30 changed the base branch from main to IAM-1686-verification-flow/handlers February 3, 2026 14:05
@abhigyanghosh30 abhigyanghosh30 force-pushed the WD-33303-add-timeout branch 2 times, most recently from d23f078 to 255b958 Compare February 3, 2026 14:29
@abhigyanghosh30 abhigyanghosh30 marked this pull request as ready for review February 3, 2026 14:30
@abhigyanghosh30 abhigyanghosh30 requested a review from a team as a code owner February 3, 2026 14:30
Copy link
Contributor

@BarcoMasile BarcoMasile left a comment

Choose a reason for hiding this comment

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

Refactor needed. This approach relies on two separate timeouts for the same logic, we can do with only one.
Some naming refactors are necessary to improve readability and make intentions clear, so please avoid vague names. Some logic wrapping is necessary here and there.
The only real change would be the count down implementation, the current one is fine but it'd be nice if we could rely on one timeout only. Also, it's important to move the timeout resend logic out of the current NodeInputSubmit component

Code lenght validation is missing from the input, we need to make sure the code is 6 digits.
Also, the email template needs to conform to the design.

In the UI I found two other issues;

  1. the input for the code is green when empty. From the design it's supposed to be white.
  2. the info message about the timeout only works once. When you resend the email it doesn't appear again.
Image Image

await setValue(attributes.value as string).then(() =>
dispatchSubmit(e),
);
if (node.meta.label?.id === 1070008) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue(readability): same here please

...flow,
ui: {
...flow.ui,
nodes: flow.ui.nodes.map((node) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The map approach here is a little overkill, since you're applying two different processing logics to the whole array with if statements to limit your action. The proper way would be to extract the nodes you're interested in, process them as you need, and then fit them back in the array. But I understand that doesn't look as "in order".

issue: I'm ok with the map approach, but the way the logic here mixes 2 different processing needs some refactor to make things clearer, for example through the use of ad-hoc functions

}

const Verification: NextPage = () => {
const UiNodePredicate = (node: UiNode) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

can be extracted from the component definition

Comment on lines 113 to 140
if (
data.state === "sent_email" &&
data.ui.messages?.find((msg) => msg.type === "error") === undefined
) {
// Check if email is sent and there is no error message
const codeUiNode = data.ui.nodes.find(UiNodePredicate);
if (codeUiNode) {
codeUiNode.messages = [
...codeUiNode.messages,
{
id: 11,
type: "success",
text: "Code sent. You can request a new one in 00:10s",
},
];
}
} else if (data.ui.messages?.find((msg) => msg.type === "error")) {
const codeUiNode = data.ui.nodes.find(UiNodePredicate);
data.ui.messages?.forEach((message) => {
if (message.type === "error") {
codeUiNode?.messages.push({
id: message.id,
type: "error",
text: "Verification code incorrect. Check your email or resend the code.",
});
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

issue(readibility): same as above, please make this clearer and more readable

Copilot AI review requested due to automatic review settings February 4, 2026 12:35
@abhigyanghosh30 abhigyanghosh30 requested a review from a team as a code owner February 4, 2026 12:35
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds UX improvements to the email verification flow by introducing a resend-code cooldown/countdown and surfacing clearer verification error/success messaging.

Changes:

  • Add resend-code cooldown behavior and success/error messaging in the verification page flow handling.
  • Introduce new UI building blocks (EmailVerificationPrompt, CountDown) and wire them into existing node renderers.
  • Misc formatting/typing refactors and update Next typed-routes reference for distDir=dist.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
ui/pages/verification.tsx Injects email prompt, alters submit handling to add success/error messages, and tweaks label appearance for resend node.
ui/components/NodeInputSubmit.tsx Adds local disabled-state + timeout logic for the resend submit control.
ui/components/NodeInputText.tsx Renders injected before/after components and adds success countdown + adjusted error handling.
ui/components/CountDown.tsx New countdown component used to render the 10s timer text.
ui/components/EmailVerificationPrompt.tsx New helper prompt shown above the code input (includes a copy typo).
ui/next-env.d.ts Updates typed routes reference path to match distDir: "dist".
ui/config/useAppConfig.tsx Formatting-only refactor.
ui/util/featureFlags.tsx Formatting-only refactor.
ui/util/replaceAuthLabel.tsx Formatting-only refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return (
<p className="u-text--muted">
An email with a verification code has been sent to {email}. Please check your
inbox and enter the code below. If the email is not recieved, ensure the
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Typo in user-facing copy: "recieved" should be "received".

Suggested change
inbox and enter the code below. If the email is not recieved, ensure the
inbox and enter the code below. If the email is not received, ensure the

Copilot uses AI. Check for mistakes.
Comment on lines 174 to 196
if (
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code"
) {
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (node.meta.label?.id === 1070008) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
}
return node;
}),
},
};
}, [flow]);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

lookupFlow maps over flow.ui.nodes but mutates each existing node (node.meta.label.context = ...) before returning it. This mutates the flow object held in React state and can lead to subtle bugs with memoization/state updates. Prefer returning new node objects when you need to adjust label context (clone node / meta / label / context as needed) instead of mutating in place.

Suggested change
if (
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code"
) {
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (node.meta.label?.id === 1070008) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
}
return node;
}),
},
};
}, [flow]);
const isCodeInputNode =
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code";
const label = node.meta?.label;
const shouldAddBeforeComponent = isCodeInputNode && !!label;
const shouldSetAppearanceLink = label?.id === 1070008;
if (!shouldAddBeforeComponent && !shouldSetAppearanceLink) {
return node;
}
const newContext = {
...label?.context,
...(shouldAddBeforeComponent && {
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
}),
...(shouldSetAppearanceLink && {
appearance: "link",
}),
};
const newLabel = {
...label,
context: newContext,
};
const newMeta = {
...node.meta,
label: newLabel,
};
return {
...node,
meta: newMeta,
};
}),
},
};
}, [flow, userEmail]);

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 4, 2026 22:13
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 21 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 130 to 141
// If no error message, add success message and disable resend button for 10 seconds
const codeUiNode = data.ui.nodes.find(UiNodePredicate);
if (codeUiNode) {
codeUiNode.messages = [
...codeUiNode.messages,
{
id: 11,
type: "success",
text: "Code sent. You can request a new one in 00:10s",
},
];
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The success message is being added to the node messages array, but it will never be displayed to the user because the countdown logic in NodeInputText.tsx (lines 90-100) replaces it with a CountDownText component. The text "Code sent. You can request a new one in 00:10s" will never be shown. Consider either removing this hardcoded text or ensuring consistency between the message text here and what's displayed by CountDownText.

Suggested change
// If no error message, add success message and disable resend button for 10 seconds
const codeUiNode = data.ui.nodes.find(UiNodePredicate);
if (codeUiNode) {
codeUiNode.messages = [
...codeUiNode.messages,
{
id: 11,
type: "success",
text: "Code sent. You can request a new one in 00:10s",
},
];
}

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +154
data.ui.messages?.forEach((message) => {
if (message.type === "error") {
codeUiNode?.messages.push({
id: message.id,
type: "error",
text: "Verification code incorrect. Check your email or resend the code.",
});
}
});
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Mutating the data object received from the API is not a good practice. The code directly modifies 'codeUiNode.messages' by pushing new messages. This can lead to unexpected side effects and makes the code harder to reason about. Consider creating a new flow object with updated messages rather than mutating the existing data structure.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 41
const beforeComponent = (
node.meta.label?.context as {
beforeComponent: Component;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The 'Component' type from React is incorrect here. It should be 'React.ReactElement' or 'React.ReactNode' since you're storing JSX elements, not component classes. The Component type refers to class-based React components, not rendered elements.

Copilot uses AI. Check for mistakes.
Comment on lines 189 to 208
if (
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code"
) {
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (isResendVerificationCode(node)) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
(node.attributes as UiNodeInputAttributes).disabled = resendDisabled;
}
return node;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Mutating the node object directly within the map function violates immutability principles. The code modifies node.meta.label.context and node.attributes.disabled in place. This can lead to unexpected side effects since the original flow object is being mutated. Instead, create and return new node objects with the updated properties.

Suggested change
if (
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code"
) {
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (isResendVerificationCode(node)) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
(node.attributes as UiNodeInputAttributes).disabled = resendDisabled;
}
return node;
let updatedNode = node;
if (
node.group === "code" &&
node.type === "input" &&
(node.attributes as UiNodeInputAttributes).name === "code"
) {
if (node.meta.label) {
updatedNode = {
...updatedNode,
meta: {
...updatedNode.meta,
label: {
...node.meta.label,
context: {
...(node.meta.label.context || {}),
beforeComponent: (
<EmailVerificationPrompt email={userEmail} />
),
},
},
},
};
}
}
if (isResendVerificationCode(node) && node.meta.label) {
const attrs = node.attributes as UiNodeInputAttributes;
updatedNode = {
...updatedNode,
meta: {
...updatedNode.meta,
label: {
...node.meta.label,
context: {
...(node.meta.label.context || {}),
appearance: "link",
},
},
},
attributes: {
...attrs,
disabled: resendDisabled,
} as UiNodeInputAttributes,
};
}
return updatedNode;

Copilot uses AI. Check for mistakes.
codeUiNode.messages = [
...codeUiNode.messages,
{
id: 11,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The hardcoded message ID '11' appears arbitrary and could conflict with actual message IDs from the backend. Consider using a unique identifier or a more appropriate approach to distinguish client-added messages from server messages, such as using a negative number or a specific prefix.

Suggested change
id: 11,
id: -1,

Copilot uses AI. Check for mistakes.
Comment on lines 64 to 88
@@ -60,30 +85,47 @@ export const NodeInputText: FC<NodeInputProps> = ({
}

return undefined;
};
}, [message, node.messages, isDuplicate, attributes.name, isWebauthn, error]);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The getError function is converted to useMemo but is now being passed directly to the error prop. Since useMemo returns the computed value (not a function), the error prop receives the error string or undefined directly. This is correct, but the naming 'getError' is misleading since it's no longer a function. Consider renaming to 'errorMessage' or similar to reflect that it's now a value, not a function.

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +205
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Potential null reference error. The code accesses node.meta.label.context without checking if node.meta.label exists first (the check on line 194 only guards the inner block). If isResendVerificationCode returns true but node.meta.label is undefined, this will throw a runtime error. Add a null check before accessing node.meta.label.context.

Suggested change
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
if (node.meta?.label) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
}

Copilot uses AI. Check for mistakes.
const emailNode = flow.ui.nodes.find(
(node) => (node.attributes as UiNodeInputAttributes).name === "email",
);
return emailNode ? (emailNode.attributes as UiNodeInputAttributes).value : "";
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The type assertion as UiNodeInputAttributes is used without checking if the node attributes are actually of type UiNodeInputAttributes. This could cause runtime errors if the node has a different attribute type. Consider adding a type guard to verify the node type before accessing the 'value' property.

Copilot uses AI. Check for mistakes.
<PageLayout title="Verify your email">
{flow ? <Flow onSubmit={handleSubmit} flow={flow} /> : <Spinner />}
<PageLayout title="Check your email">
{flow ? <Flow onSubmit={handleSubmit} flow={lookupFlow} /> : <Spinner />}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

This use of variable 'flow' always evaluates to true.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 5, 2026 09:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 47 to 53
const disableButtonWithTimeout = () => {
setResendDisabled(true);
const timer = setTimeout(() => {
setResendDisabled(false);
}, RESEND_CODE_TIMEOUT);
return () => clearTimeout(timer);
};
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Potential memory leak: The cleanup function returned from disableButtonWithTimeout is never used. When this function is called, it returns a cleanup function but that return value is ignored. This means the timer is never cleared if the component unmounts while the timer is active.

Copilot uses AI. Check for mistakes.
}>;
}
).continue_with;
if (continue_with[0].action === "redirect_browser_to") {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Array access without bounds checking: The code accesses continue_with[0] without verifying that the array has at least one element. If continue_with is an empty array, this will result in a runtime error. Add a check to ensure the array is not empty before accessing its first element.

Suggested change
if (continue_with[0].action === "redirect_browser_to") {
if (
continue_with.length > 0 &&
continue_with[0].action === "redirect_browser_to"
) {

Copilot uses AI. Check for mistakes.
<PageLayout title="Verify your email">
{flow ? <Flow onSubmit={handleSubmit} flow={flow} /> : <Spinner />}
<PageLayout title="Check your email">
{flow ? <Flow onSubmit={handleSubmit} flow={lookupFlow} /> : <Spinner />}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

This use of variable 'flow' always evaluates to true.

Suggested change
{flow ? <Flow onSubmit={handleSubmit} flow={lookupFlow} /> : <Spinner />}
<Flow onSubmit={handleSubmit} flow={lookupFlow} />

Copilot uses AI. Check for mistakes.
@BarcoMasile
Copy link
Contributor

BarcoMasile commented Feb 5, 2026

Before I review the changes, I tested the code and still there are a few issues

UI related

the input is still green.
image

Design related

This doesn't seem matching the design, it's also wrong because this happens after the verification finished successfully.
image

initialSeconds?: number;
}) => {
const [seconds, setSeconds] = useState(initialSeconds);
console.log("CountDownText rendered with seconds:", initialSeconds);
Copy link
Contributor

Choose a reason for hiding this comment

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

question: can we remove these console.logs?

];
}
// Disable resend button for 10 seconds
disableButtonWithTimeout();
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: as copilot states, this function returns another function that needs to be called, but it's not, effectively non clearing the timer.

@BarcoMasile BarcoMasile force-pushed the IAM-1686-verification-flow/handlers branch from f9ab970 to 33770c7 Compare February 9, 2026 14:12
Copy link
Contributor

@natalian98 natalian98 left a comment

Choose a reason for hiding this comment

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

I got the following lint error when building the app with make npm-build build:

Failed to compile.

./pages/verification.tsx
234:16  Error: Replace `·title={flow.state==="passed_challenge"?"Verification·successful":"Check·your·email"}` with `⏎······title={⏎········flow.state·===·"passed_challenge"⏎··········?·"Verification·successful"⏎··········:·"Check·your·email"⏎······}⏎····`  prettier/prettier

Copy link
Contributor

Choose a reason for hiding this comment

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

Email address validation is not working properly. For example, if you type test@e no error is displayed.

return (
<p className="u-text--muted">
An email with a verification code has been sent to {email}. Please check
your inbox and enter the code below. If the email is not recieved, ensure
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
your inbox and enter the code below. If the email is not recieved, ensure
your inbox and enter the code below. If the email is not received, ensure

Copy link
Contributor

Choose a reason for hiding this comment

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

Some questions about the successful verification scenario.
According to the design we should get a message "You will be redirected to ..." which should then redirect to return_to url if included in parameters. Can we implement this? For reference, we have this kind of pattern for setup complete page.

Image

Right now after successful verification, I get redirected to a page with Continue button:

Image

Upon clicking on it I get to /ui which doesn't exist. Could we instead redirect to manage_details page if return_to parameter is not specified?

@a-gorle
Copy link

a-gorle commented Feb 9, 2026

Please increase the timer to 60s instead of the current 10s, to allow for reasonable time for the code to be sent from the server and 10s is too short for the purpose of having a cool off time to avoid any abuse of continuous activity on the resend code button.

@BarcoMasile BarcoMasile force-pushed the IAM-1686-verification-flow/handlers branch 2 times, most recently from 265a37c to 6a86de7 Compare February 9, 2026 15:17
Copilot AI review requested due to automatic review settings February 10, 2026 13:42
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 14 changed files in this pull request and generated 7 comments.

Files not reviewed (1)
  • ui/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +107 to +126
const resendButton = flow.ui.nodes.find(isResendVerificationCode);
return (
<>
<span>{getNodeLabel(node)}</span>
<Button
appearance={"link"}
tabIndex={4}
onClick={async (e) => {
e.preventDefault();
// On click, we set this value, and once set, dispatch the submission!
await setValue(
(resendButton?.attributes as UiNodeInputAttributes)
.value as string,
resendButton ? getNodeId(resendButton) : undefined,
).then(() => dispatchSubmit(e));
}}
style={{ float: "right", marginBottom: 0 }}
disabled={
(resendButton?.attributes as UiNodeInputAttributes).disabled
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

In the resend button label, resendButton can be undefined, but the code still dereferences (resendButton?.attributes as UiNodeInputAttributes).value/disabled. Because the optional chain only applies to attributes, this can still evaluate to undefined.disabled/undefined.value and crash. Guard for a missing resend node (e.g., don’t render the button or make it disabled) before reading its attributes.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +74
useEffect(() => {
if (message) {
setInputValue(message);
}
}, [message, setInputValue]);

const getError = () => {
const beforeComponent = (
node.meta.label?.context as {
beforeComponent: React.ReactNode;
}
)?.beforeComponent;

const afterComponent = (
node.meta.label?.context as {
afterComponent: React.ReactNode;
}
)?.afterComponent;

useEffect(() => {
if (node.messages.length === 0) {
return;
}
for (const msg of node.messages) {
if (msg.type !== "info") {
return;
}
}
if (message) {
setInputValue(message);
}
}, [message, setInputValue]);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The new useEffect that checks node.messages (info-only) doesn’t include node.messages in its dependency list, so it won’t rerun when messages change (which is the main trigger for this logic). Also, the earlier useEffect already sets inputValue for any non-empty message, which makes the info-only effect ineffective and can overwrite user input with error text. Consider removing/conditioning the first effect and add node.messages (or message computed from it) to the dependency list of the remaining effect.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +57
const [resendDisabled, setResendDisabled] = useState<boolean>(false);
const disableButtonWithTimeout = () => {
setResendDisabled(true);

const timer = setTimeout(() => {
setResendDisabled(false);
clearTimeout(timer);
}, RESEND_CODE_TIMEOUT);
return () => clearTimeout(timer);
};
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

disableButtonWithTimeout returns a cleanup function but the return value is never used, so the timeout won’t be cleared on unmount. This can lead to state updates on an unmounted component and flaky behavior if the user navigates away. Store the timeout id in a ref and clear it in a useEffect cleanup (and avoid calling clearTimeout from inside the timeout callback).

Copilot uses AI. Check for mistakes.
Comment on lines 129 to 167
if (
data.state === "sent_email" &&
data.ui.messages?.find((msg) => msg.type === "error") === undefined
) {
// Check if email is sent and there is no error message
// If no error message, add success message and disable resend button for 10 seconds
const codeUiNode = data.ui.nodes.find(UiNodePredicate) as UiNode;
if (codeUiNode) {
codeUiNode.meta = {
...codeUiNode.meta,
label: {
...codeUiNode.meta.label,
context: {
...codeUiNode.meta.label?.context,
afterComponent: (
<CountDownText
initialSeconds={RESEND_CODE_TIMEOUT / 1000}
wrapperText="Code sent. You can request again in 00:"
key={new Date().toISOString()}
/>
),
},
},
} as UiNodeMeta;
}
// Disable resend button for 10 seconds
disableButtonWithTimeout();
} else if (data.ui.messages?.find((msg) => msg.type === "error")) {
const codeUiNode = data.ui.nodes.find(UiNodePredicate);
data.ui.messages?.forEach((message) => {
if (message.type === "error") {
codeUiNode?.messages.push({
id: message.id,
type: "error",
text: "Verification code incorrect. Check your email or resend the code.",
});
}
});
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

There are Playwright specs for other auth flows under ui/tests, but the new verification resend cooldown / countdown behavior has no automated coverage. Add a Playwright test for /ui/verification that asserts the resend control disables for ~10s and re-enables, and that an invalid code shows the new error text.

Copilot uses AI. Check for mistakes.
return (
<p className="u-text--muted">
An email with a verification code has been sent to {email}. Please check
your inbox and enter the code below. If the email is not recieved, ensure
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

Typo: “recieved” should be “received”.

Suggested change
your inbox and enter the code below. If the email is not recieved, ensure
your inbox and enter the code below. If the email is not received, ensure

Copilot uses AI. Check for mistakes.
{children}
</AppConfigContext>
)
return <AppConfigContext value={contextValue}>{children}</AppConfigContext>;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

AppConfigProvider is rendering the context object directly (<AppConfigContext ...>) instead of the provider (<AppConfigContext.Provider ...>). This will throw at runtime (invalid element type) and prevents consumers from receiving the context value. Update the return to use AppConfigContext.Provider (and close the matching tag).

Suggested change
return <AppConfigContext value={contextValue}>{children}</AppConfigContext>;
return (
<AppConfigContext.Provider value={contextValue}>
{children}
</AppConfigContext.Provider>
);

Copilot uses AI. Check for mistakes.
: "Check your email"
}
>
{flow ? <Flow onSubmit={handleSubmit} flow={lookupFlow} /> : <Spinner />}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This use of variable 'flow' always evaluates to true.

Suggested change
{flow ? <Flow onSubmit={handleSubmit} flow={lookupFlow} /> : <Spinner />}
<Flow onSubmit={handleSubmit} flow={lookupFlow} />

Copilot uses AI. Check for mistakes.
@BarcoMasile BarcoMasile force-pushed the IAM-1686-verification-flow/handlers branch 2 times, most recently from 9a2fce7 to 5f346a2 Compare February 11, 2026 10:00
Copilot AI review requested due to automatic review settings February 11, 2026 13:17
function setFlowIDQueryParam(router: NextRouter, flowId: string) {
void router.push(
{
pathname: router.pathname,
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to remove this to fix the paths issue, otherwise any request to ui/verification is rewritten to /verification and you can't refresh the page

Suggested change
pathname: router.pathname,

Copy link
Contributor

@nsklikas nsklikas left a comment

Choose a reason for hiding this comment

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

Not a frontend engineer so I will not comment on the code, but there are some issues with the implementation.

Run some tests on my own and also used antigravity to test it, this is the report generated from antigravity:

Summary

The PR successfully implements the core "countdown" and "disable button" logic. However, several critical issues regarding state persistence, routing, and user experience were identified during testing. These issues make the feature fragile and easily bypassable.

✅ Verified Features

  • Countdown Timer: Correctly initialized when valid code is sent.
  • Resend Disable: Button is correctly disabled during countdown.
  • Input Validation: Invalid codes are correctly rejected with appropriate error messages.
  • Success Flow: Valid codes lead to successful verification and redirection.

⚠️ Issues & Change Requests

1. Countdown Persistence (Critical)

  • Issue: The countdown functionality relies entirely on the temporary page state. Reloading the page resets the state, effectively clearing the countdown.
  • Impact: Users can bypass the resend throttle by simply refreshing the browser.
  • Recommendation: Persist the timestamp (e.g., in localStorage or sessionStorage) so the countdown resumes correctly after a reload.

2. Routing & URL Structure

  • Issue: During the verification flow, the browser URL redirects to /verification?flow=..., dropping the necessary /ui prefix.
  • Impact: If a user reloads the page while in the flow, they receive a 404 Not Found error because the application is served under /ui.
  • Recommendation: Ensure all redirects and router pushes maintain the /ui prefix.

3. Back Button Behavior

  • Issue: Clicking the browser's "Back" button from the code entry screen changes the URL but does not update the UI (it remains on the code entry screen). Subsequent clicks lead to 404 errors.
  • Impact: Users cannot navigate back to correct a typo in their email address.
  • Recommendation: Ensure the router history is correctly managed (e.g., using replace instead of push where appropriate, or handling popstate events) to allow proper navigation.

4. Response Message UX

  • Issue: The success notification displays: "You will be redirected to /ui/manage_details".
  • Impact: This exposes internal URL paths to the end-user, looking unpolished.
  • Recommendation: Update the text to a user-friendly message, such as "You will be redirected to your dashboard" or simply "Redirecting...".

5. Email Link

  • Note: The verification link sent in emails (/self-service/verification...) is incorrect and leads to a 404.

import CountDownText from "../components/CountDownText";

function setFlowIDQueryParam(router: NextRouter, flowId: string) {
void router.push(
Copy link
Contributor

Choose a reason for hiding this comment

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

question: In some other places I see that replaceState is used. AFAICT replaceState replaces the entry in history where as a push adds a new entry. Maybe this is a different case and using push makes sense.

function setFlowIDQueryParam(router: NextRouter, flowId: string) {
void router.push(
{
pathname: router.pathname,
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: This is wrong. AFAICT you are setting the browser path based on the pathname in the nextjs router. These 2 paths are not the same, nextjs cannot know the actual route of the page as most of the time the files are served using a reverse proxy.

For example if you go to the verification page and try to refresh you will get a 404 error.

Comment on lines +109 to +137
if (isVerificationCodeInput(node)) {
const resendButton = flow.ui.nodes.find(isResendVerificationCode);
return (
<>
<span>{getNodeLabel(node)}</span>
<Button
appearance={"link"}
tabIndex={4}
onClick={async (e) => {
e.preventDefault();
// On click, we set this value, and once set, dispatch the submission!
await setValue(
(resendButton?.attributes as UiNodeInputAttributes)
.value as string,
resendButton ? getNodeId(resendButton) : undefined,
).then(() => dispatchSubmit(e));
}}
style={{ float: "right", marginBottom: 0 }}
disabled={
(resendButton?.attributes as UiNodeInputAttributes).disabled
}
>
Resend code
</Button>
</>
);
}
return getNodeLabel(node);
}, [node, flow]);
Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Not sure if this is the correct approach. If I go to the verification page and input my email, I will get an email and the Resend code button will be unclickable. BUT if I just refresh the page (I have to change the path because of the bug with router.push), I can click the resend button even if 1min has not passed. Not sure how this should be implemented.

@BarcoMasile BarcoMasile force-pushed the IAM-1686-verification-flow/handlers branch from 5f346a2 to a2babee Compare February 16, 2026 14:57
afterComponent: (
<CountDownText
initialSeconds={RESEND_CODE_TIMEOUT / 1000}
wrapperText="Code sent. You can request again in 00:"
Copy link
Contributor

Choose a reason for hiding this comment

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

The countdown is gone after you refresh the page.
also a nit: At the beginning of the countdown, 00:60s is shown. Should be 01:00 perhaps?

@abhigyanghosh30 abhigyanghosh30 changed the base branch from IAM-1686-verification-flow/handlers to main February 16, 2026 16:46
Copilot AI review requested due to automatic review settings February 16, 2026 18:13
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 16 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • ui/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const [hasLocalValidation, setLocalValidation] = React.useState(false);

const emailRegex = /^[^\s@]+@[^\s@]+$/;
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The email regex limits TLD (top-level domain) to 2-6 characters, but some valid TLDs can be longer (e.g., ".construction" has 12 characters). Consider using a more flexible pattern like {2,} to allow TLDs of any length 2 or greater, or verify that the 6-character limit aligns with your application's requirements.

Suggested change
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +47

const timer = setTimeout(() => {
setResendDisabled(false);
clearTimeout(timer);
}, RESEND_CODE_TIMEOUT);
return () => clearTimeout(timer);
};

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The cleanup function returned from disableButtonWithTimeout will never be used because it's not being returned from a useEffect or stored anywhere. The function is called directly (line 157), not used as an effect cleanup. This means the timer could potentially continue running even after the component unmounts, leading to a memory leak. Consider moving this logic into a useEffect hook with proper cleanup, or remove the unused return statement.

Suggested change
const timer = setTimeout(() => {
setResendDisabled(false);
clearTimeout(timer);
}, RESEND_CODE_TIMEOUT);
return () => clearTimeout(timer);
};
};
useEffect(() => {
if (!resendDisabled) {
return;
}
const timer = setTimeout(() => {
setResendDisabled(false);
}, RESEND_CODE_TIMEOUT);
return () => {
clearTimeout(timer);
};
}, [resendDisabled]);

Copilot uses AI. Check for mistakes.
window.location.href = returnTo;
} else {
const timer = setTimeout(() => {
clearTimeout(timer);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Calling clearTimeout immediately after setting the timeout is redundant since the timeout has already fired at that point. The clearTimeout call on line 107 has no effect and can be removed.

Suggested change
clearTimeout(timer);

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +157
// Disable resend button for 10 seconds
disableButtonWithTimeout();
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The comment says "Disable resend button for 10 seconds" but the actual timeout is RESEND_CODE_TIMEOUT which is 60000ms (60 seconds). Update the comment to match the actual timeout duration.

Copilot uses AI. Check for mistakes.

const timer = setTimeout(() => {
setResendDisabled(false);
clearTimeout(timer);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Calling clearTimeout inside the setTimeout callback is redundant since the timeout has already fired at that point. The clearTimeout call on line 43 has no effect and can be removed. The cleanup function returned on line 45 would be the proper place to clear the timeout if it were actually used.

Suggested change
clearTimeout(timer);

Copilot uses AI. Check for mistakes.
setInputValue(newValue);
void setValue(newValue);
},
[setValue],
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The handleChange callback is missing node and setInputValue in its dependency array. The callback uses node to check if it's a verification code input and uses setInputValue to update local state, but only includes setValue in the dependency array. This could lead to stale closures. Add node and setInputValue to the dependency array.

Suggested change
[setValue],
[setValue, node, setInputValue],

Copilot uses AI. Check for mistakes.
… with other routes. Timer shows minutes and seconds in countdown text
Copilot AI review requested due to automatic review settings February 16, 2026 22:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 9 comments.

Files not reviewed (1)
  • ui/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

ui/pages/verification.tsx:88

  • Missing dependency in useEffect: The effect on line 53 uses verificationCode (line 62) but doesn't include it in the dependency array on line 88. This could cause the effect to use stale values if verificationCode changes. Add verificationCode to the dependency array.
  useEffect(() => {
    if (!router.isReady || flow) {
      return;
    }

    if (flowId) {
      kratos
        .getVerificationFlow({ id: String(flowId) })
        .then(({ data }) => {
          if (verificationCode) {
            const codeUiNode = data.ui.nodes.find(isVerificationCodeInput);
            if (codeUiNode) {
              (codeUiNode.attributes as UiNodeInputAttributes).value =
                String(verificationCode);
            }
          }
          setFlowIDQueryParam(String(data.id));
          setFlow(data);
        })
        .catch(handleFlowError("verification", setFlow))
        .catch(redirectToErrorPage);

      return;
    }

    kratos
      .createBrowserVerificationFlow({
        returnTo: returnTo ? String(returnTo) : undefined,
      })
      .then(({ data }) => {
        setFlow(data);
        setFlowIDQueryParam(String(data.id));
      })
      .catch(handleFlowError("verification", setFlow))
      .catch(redirectToErrorPage);
  }, [flowId, router, router.isReady, returnTo]);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

severity="positive"
borderless
className="u-no-margin--bottom"
>{`${wrapperText}${seconds >= 60 ? `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, "0")}` : `${seconds.toString().padStart(2, "0")}s`}`}</Notification>
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The long inline ternary expression on line 34 is difficult to read and maintain. Consider extracting the time formatting logic into a separate function for better readability. For example, create a formatTime function that takes seconds and returns the formatted string.

Copilot uses AI. Check for mistakes.
setResendDisabled(false);
clearTimeout(timer);
}, RESEND_CODE_TIMEOUT);
return () => clearTimeout(timer);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Memory leak: The cleanup function returns the result of clearTimeout, which should return void. The cleanup function should not return anything. Remove the return statement on line 45.

Suggested change
return () => clearTimeout(timer);

Copilot uses AI. Check for mistakes.

const getLabel = useMemo(() => {
if (isVerificationCodeInput(node)) {
const resendButton = flow.ui.nodes.find(isResendVerificationCode);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Unsafe context access: On line 110, the code accesses flow.ui.nodes without checking if flow is properly initialized. Given that FlowContext is initialized with an empty object cast to the flow types (see FlowContext.tsx:10-11), this will cause a runtime error when trying to access ui.nodes on an empty object. Add a null check or ensure this component is only rendered within a proper FlowContext.Provider.

Suggested change
const resendButton = flow.ui.nodes.find(isResendVerificationCode);
const resendButton = flow?.ui?.nodes?.find(isResendVerificationCode);

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +126
<Button
appearance={"link"}
tabIndex={4}
onClick={async (e) => {
e.preventDefault();
// On click, we set this value, and once set, dispatch the submission!
await setValue(
(resendButton?.attributes as UiNodeInputAttributes)
.value as string,
resendButton ? getNodeId(resendButton) : undefined,
).then(() => dispatchSubmit(e));
}}
style={{ float: "right", marginBottom: 0 }}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The "Resend code" button uses inline styles with float: "right" (line 126), which can cause accessibility issues with screen readers as the visual order differs from the DOM order. The button appears after the label text in the DOM but is displayed on the right side visually. Consider using CSS flexbox or grid for proper layout instead of float to maintain consistency between visual and DOM order.

Copilot uses AI. Check for mistakes.
});
}, 1000);
return () => clearInterval(timerId);
}, [initialSeconds]);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The initialSeconds dependency in the useEffect on line 23 will cause the timer to restart if initialSeconds changes. Since this prop shouldn't change during the component's lifecycle, this dependency should be removed to prevent unnecessary timer resets. Use an empty dependency array [] instead.

Suggested change
}, [initialSeconds]);
}, []);

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +231
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (isResendVerificationCode(node)) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
(node.attributes as UiNodeInputAttributes).disabled =
resendDisabled;
}
return node;
}),
},
};
}, [flow, resendDisabled]);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Direct mutation of node object: The code directly mutates the node.meta.label.context object (lines 213-216 and 220-225) within a useMemo. This is problematic because: 1) It mutates the original flow data structure, which could cause issues if the flow object is used elsewhere, and 2) Mutations inside useMemo violate React's principles and can lead to unpredictable behavior. Instead, create a deep copy of the node before mutating it, or restructure this to return a new object without mutations.

Suggested change
if (node.meta.label) {
node.meta.label.context = {
...node.meta.label.context,
beforeComponent: <EmailVerificationPrompt email={userEmail} />,
};
}
}
if (isResendVerificationCode(node)) {
node.meta.label.context = {
...node.meta.label.context,
appearance: "link",
};
(node.attributes as UiNodeInputAttributes).disabled =
resendDisabled;
}
return node;
}),
},
};
}, [flow, resendDisabled]);
const label = node.meta?.label;
if (!label) {
return node;
}
return {
...node,
meta: {
...node.meta,
label: {
...label,
context: {
...label.context,
beforeComponent: (
<EmailVerificationPrompt email={userEmail} />
),
},
},
},
};
}
if (isResendVerificationCode(node)) {
const attrs = node.attributes as UiNodeInputAttributes;
return {
...node,
meta: {
...node.meta,
label: {
...node.meta?.label,
context: {
...node.meta?.label?.context,
appearance: "link",
},
},
},
attributes: {
...attrs,
disabled: resendDisabled,
} as UiNodeInputAttributes,
};
}
return node;
}),
},
};
}, [flow, resendDisabled, userEmail]);

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +110
const timer = setTimeout(() => {
clearTimeout(timer);
window.location.href = "/ui/manage_details";
}, 3000);
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Timer cleanup issue: The timer created on line 106 is stored in a variable but never cleaned up if the component unmounts before the 3-second timeout completes. This could cause the redirect to execute after unmount or cause a memory leak. Consider storing the timer ID in state or a ref and clearing it in a cleanup function.

Copilot uses AI. Check for mistakes.
}),
},
};
}, [flow, resendDisabled]);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The lookupFlow useMemo on line 198 depends on userEmail (used on line 215), but userEmail is not included in the dependency array on line 231. This could cause stale data to be used. Add userEmail to the dependency array.

Suggested change
}, [flow, resendDisabled]);
}, [flow, resendDisabled, userEmail]);

Copilot uses AI. Check for mistakes.
{shallow: true},
);
export function setFlowIDQueryParam(flowId: string) {
window.history.replaceState(null, "", `?flow=${flowId}`);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The refactored setFlowIDQueryParam function replaces the entire query string with just the flow parameter using window.history.replaceState(null, "", "?flow=${flowId}"). This will remove any other existing query parameters (like return_to or code). The original implementation preserved other query parameters by spreading router.query. This could break functionality where multiple query parameters need to coexist. Consider preserving existing query parameters using URLSearchParams or similar approach.

Suggested change
window.history.replaceState(null, "", `?flow=${flowId}`);
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("flow", flowId);
const newUrl =
window.location.pathname +
"?" +
searchParams.toString() +
window.location.hash;
window.history.replaceState(null, "", newUrl);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants