Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
*/

import { RequestErrorInterface, RequestResultInterface } from "@wso2is/admin.core.v1/hooks/use-request";
import useGetExtensionExecutorSteps
from "@wso2is/admin.flow-builder-core.v1/api/use-get-extension-executor-steps";
import useGetFlowBuilderCoreResources from "@wso2is/admin.flow-builder-core.v1/api/use-get-flow-builder-core-resources";
import { Resources } from "@wso2is/admin.flow-builder-core.v1/models/resources";
import { Step } from "@wso2is/admin.flow-builder-core.v1/models/steps";
import { FlowTypes } from "@wso2is/admin.flows.v1/models/flows";
import elements from "../data/elements.json";
import steps from "../data/steps.json";
import templates from "../data/templates.json";
Expand All @@ -40,6 +44,9 @@ const useGetAskPasswordFlowBuilderResources = <Data = Resources, Error = Request
): RequestResultInterface<Data, Error> => {
const { data: coreResources } = useGetFlowBuilderCoreResources();

// Executors contributed by extensions deployed on the server. Appended to the palette at runtime.
const extensionExecutorSteps: Step[] = useGetExtensionExecutorSteps(FlowTypes.INVITED_USER_REGISTRATION);

return {
data: ({
...coreResources,
Expand All @@ -49,7 +56,8 @@ const useGetAskPasswordFlowBuilderResources = <Data = Resources, Error = Request
],
steps: [
...coreResources?.steps,
...steps
...steps,
...extensionExecutorSteps
],
templates: [
...coreResources?.templates,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { FieldKey, FieldValue } from "@wso2is/admin.flow-builder-core.v1/models/
import { Element, ElementCategories, ElementTypes } from "@wso2is/admin.flow-builder-core.v1/models/elements";
import { Resource } from "@wso2is/admin.flow-builder-core.v1/models/resources";
import { ExecutionTypes, StepCategories, StepTypes } from "@wso2is/admin.flow-builder-core.v1/models/steps";
import { useIsConnectionlessExtensionExecutor }
from "@wso2is/admin.flow-builder-core.v1/hooks/use-extension-executor";
import { IdentifiableComponentInterface } from "@wso2is/core/models";
import isEmpty from "lodash-es/isEmpty";
import React, { ChangeEvent, FunctionComponent, ReactElement, useMemo } from "react";
Expand Down Expand Up @@ -61,6 +63,10 @@ const ResourceProperties: FunctionComponent<ResourcePropertiesPropsInterface> =
return resource?.variants?.find((_element: Element) => _element.variant === resource.variant);
}, [ resource.variants, resource.variant ]);

const isConnectionlessExtensionExecutor: boolean = useIsConnectionlessExtensionExecutor(
resource?.data?.action?.executor?.name
);

const renderElementId = (): ReactElement => {
return (
<ResourcePropertyFactory
Expand Down Expand Up @@ -228,6 +234,7 @@ const ResourceProperties: FunctionComponent<ResourcePropertiesPropsInterface> =
<>
{ renderElementId() }
{
!isConnectionlessExtensionExecutor &&
!AskPasswordFlowBuilderConstants.FEDERATION_CONFIG_SKIPPED_EXECUTORS.includes(
resource?.data?.action?.executor?.name) && (
<FederationProperties
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { FlowTypes } from "@wso2is/admin.flows.v1/models/flows";
import { useMemo } from "react";
import useGetMetadata from "./use-metadata";
import { MetadataInterface } from "../models/metadata";
import { Step } from "../models/steps";
import buildExtensionExecutorSteps from "../utils/build-extension-executor-steps";

/**
* Hook that returns the palette steps for executors contributed by extensions deployed on the server.
*/
const useGetExtensionExecutorSteps = (flowType: FlowTypes): Step[] => {
const { data: metadata } = useGetMetadata<MetadataInterface>(flowType, !!flowType);

return useMemo(
() => buildExtensionExecutorSteps(metadata?.extensionExecutors),
[ metadata?.extensionExecutors ]
);
};

export default useGetExtensionExecutorSteps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import Box from "@oxygen-ui/react/Box";
import Typography from "@oxygen-ui/react/Typography";
import loadStaticResource from "@wso2is/admin.core.v1/utils/load-static-resource";
import { IdentifiableComponentInterface } from "@wso2is/core/models";
import React, { ReactElement, useMemo, useState } from "react";
import useAuthenticationFlowBuilderCore from "../../../../../hooks/use-authentication-flow-builder-core-context";
import { ExtensionExecutorInterface } from "../../../../../models/metadata";
import { DEFAULT_EXTENSION_EXECUTOR_ICON } from "../../../../../utils/build-extension-executor-steps";
import { ExecutionMinimalPropsInterface } from "../execution-minimal";

/**
* Props interface of ExtensionExecution.
*/
type ExtensionExecutionPropsInterface = ExecutionMinimalPropsInterface & IdentifiableComponentInterface;

/**
* Canvas node for an executor contributed by an extension deployed on the server.
*/
const ExtensionExecution = ({
resource,
"data-componentid": componentId = "extension-execution"
}: ExtensionExecutionPropsInterface): ReactElement => {
const { metadata } = useAuthenticationFlowBuilderCore();
const [ hasIconFailed, setHasIconFailed ] = useState<boolean>(false);

const executorName: string = resource?.data?.action?.executor?.name;

const executor: ExtensionExecutorInterface | null = useMemo(() => {
if (!executorName || !metadata?.extensionExecutors?.length) {
return null;
}

return metadata.extensionExecutors.find(
(candidate: ExtensionExecutorInterface) => candidate.name === executorName
) || null;
}, [ executorName, metadata?.extensionExecutors ]);

const isDefaultIcon: boolean = hasIconFailed || !executor?.icon;
const iconSrc: string = isDefaultIcon
? loadStaticResource(DEFAULT_EXTENSION_EXECUTOR_ICON)
: loadStaticResource(executor.icon);

const displayName: string = executor?.displayName || resource?.display?.label || executorName;

return (
<Box
display="flex"
gap={ 1 }
data-componentid={ componentId }
className="flow-builder-execution extension-execution"
>
<img
src={ iconSrc }
alt={ displayName }
height="20"
style={ { filter: isDefaultIcon ? "invert(1)" : "none", objectFit: "contain" } }
onError={ () => setHasIconFailed(true) }
/>
<Typography variant="body1">{ displayName }</Typography>
</Box>
);
};

export default ExtensionExecution;
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import { IdentifiableComponentInterface } from "@wso2is/core/models";
import React, { FC, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import AppleExecution from "./apple-execution";
import ExtensionExecution from "./extension-execution";
import FacebookExecution from "./facebook-execution";
import GithubExecution from "./github-execution";
import GoogleExecution from "./google-execution";
import FlowExtensionExecution from "./flow-extension-execution";
import MicrosoftExecution from "./microsoft-execution";
import useAuthenticationFlowBuilderCore from "../../../../../hooks/use-authentication-flow-builder-core-context";
import { ExtensionExecutorInterface } from "../../../../../models/metadata";
import { ExecutionTypes } from "../../../../../models/steps";
import "./execution-factory.scss";
import { ExecutionMinimalPropsInterface } from "../execution-minimal";
Expand All @@ -48,6 +51,7 @@ const ExecutionFactory: FC<ExecutionFactoryPropsInterface> = ({
"data-componentid": componentId = "execution-factory"
}: ExecutionFactoryPropsInterface): ReactElement => {
const { t } = useTranslation();
const { metadata } = useAuthenticationFlowBuilderCore();

if ((resource.data?.action as any)?.executor?.name === ExecutionTypes.GoogleFederation) {
return (
Expand Down Expand Up @@ -128,6 +132,20 @@ const ExecutionFactory: FC<ExecutionFactoryPropsInterface> = ({
);
}

/*
* Flow metadata describes how to render executors that are dynamically registered.
*/
const isContributedByExtension: boolean = !!metadata?.extensionExecutors?.some(
(executor: ExtensionExecutorInterface) =>
executor.name === (resource.data?.action as any)?.executor?.name
);

if (isContributedByExtension) {
return (
<ExtensionExecution resource={ resource } />
);
}

return (
<Box display="flex" gap={ 1 } data-componentid={ componentId }>
<Typography variant="body1">{ t("flows:core.executions.names.default") }</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class ValidationConstants {
private constructor() {}

public static readonly REQUIRED_FIELD_ERROR_CODE: string = "REQUIRED_FIELD_ERROR";

/**
* Behavior flag an executor declares to quality as a password recovery factor.
*/
public static readonly RECOVERY_FACTOR_BEHAVIOR_FLAG: string = "RECOVERY_FACTOR";
}

export default ValidationConstants;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { useMemo } from "react";
import useAuthenticationFlowBuilderCore from "./use-authentication-flow-builder-core-context";
import { ExtensionExecutorInterface } from "../models/metadata";

/**
* Hook that resolves the metadata of an executor contributed by an extension deployed on the server.
*/
const useExtensionExecutor = (executorName: string): ExtensionExecutorInterface | null => {
const { metadata } = useAuthenticationFlowBuilderCore();

return useMemo(() => {
if (!executorName || !metadata?.extensionExecutors?.length) {
return null;
}

return metadata.extensionExecutors.find(
(executor: ExtensionExecutorInterface) => executor.name === executorName
) || null;
}, [ executorName, metadata?.extensionExecutors ]);
};

/**
* Hook that decides whether the connection picker applies to an dynamically registered executor.
*/
export const useIsConnectionlessExtensionExecutor = (executorName: string): boolean => {
const executor: ExtensionExecutorInterface | null = useExtensionExecutor(executorName);

return !!executor && executor.requiresConnection === false;
};
48 changes: 36 additions & 12 deletions features/admin.flow-builder-core.v1/hooks/use-factor-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ RegistrationFlowExecutorConstants
import { Edge, Node, ReactFlowState, getIncomers, useStore } from "@xyflow/react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import useAuthenticationFlowBuilderCore from "./use-authentication-flow-builder-core-context";
import useValidationStatus from "./use-validation-status";
import ValidationConstants from "../constants/validation-constants";
import { Action, Element } from "../models/elements";
import { ExtensionExecutorInterface } from "../models/metadata";
import Notification, { NotificationType } from "../models/notification";
import { Resource } from "../public-api";

Expand All @@ -33,6 +36,7 @@ import { Resource } from "../public-api";
const useRecoveryFactorValidation = (node: Node): void => {
const { t } = useTranslation();
const { addNotification, removeNotification, validationConfig } = useValidationStatus();
const { metadata } = useAuthenticationFlowBuilderCore();

const nodes: Node[] = useStore((state: ReactFlowState) => state.nodes);
const edges: Edge[] = useStore((state: ReactFlowState) => state.edges);
Expand Down Expand Up @@ -76,19 +80,40 @@ const useRecoveryFactorValidation = (node: Node): void => {
};

/**
* Finds factor views such as SMS OTP, Email OTP and Magic Link in a node's components.
* Executors that satisfy the recovery factor requirement. Covers the built-in factors and any
* executor contributed by a deployed extension that declares the RECOVERY_FACTOR behavior flag.
*/
const recoveryFactorExecutors: Set<string> = useMemo(() => {
const factors: Set<string> = new Set<string>([
RegistrationFlowExecutorConstants.EMAIL_OTP_EXECUTOR,
RegistrationFlowExecutorConstants.SMS_OTP_EXECUTOR,
RegistrationFlowExecutorConstants.MAGIC_LINK_EXECUTOR
]);

metadata?.extensionExecutors?.forEach((executor: ExtensionExecutorInterface) => {
if (executor?.name
&& executor?.behaviorFlags?.includes(ValidationConstants.RECOVERY_FACTOR_BEHAVIOR_FLAG)) {
factors.add(executor.name);
}
});

return factors;
}, [ metadata?.extensionExecutors ]);

/**
* Finds factor views such as SMS OTP, Email OTP, Magic Link or an extension contributed
* recovery factor in a node's components or in its own action.
*/
const FactorExistsInTheFlow = (node: Node): boolean => {

const isRecoveryFactor = (executorName: string): boolean =>
!!executorName && recoveryFactorExecutors.has(executorName);

return (node?.data?.components as Element[])?.some((parent: Element) =>
parent?.components?.some(
(child: Element) => child?.action?.executor?.name ===
RegistrationFlowExecutorConstants.EMAIL_OTP_EXECUTOR ||
child?.action?.executor?.name ===
RegistrationFlowExecutorConstants.SMS_OTP_EXECUTOR
(child: Element) => isRecoveryFactor(child?.action?.executor?.name)
)
) || (node?.data?.action as Action)?.executor?.name ===
RegistrationFlowExecutorConstants.MAGIC_LINK_EXECUTOR;
) || isRecoveryFactor((node?.data?.action as Action)?.executor?.name);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

/**
Expand All @@ -104,7 +129,7 @@ const useRecoveryFactorValidation = (node: Node): void => {
const hasAncestors: boolean = ancestors?.length > 0;

/**
* Checks if any ancestor nodes have factor views such as SMS OTP, Email OTP and Magic Link.
* Checks if any ancestor nodes have a recovery factor view.
*/
const checkIfAncestorsHaveFactors: boolean = useMemo((): boolean => {
if (!isRecoveryFactorValidationEnabled || !containsResetPassword || !node) return false;
Expand All @@ -116,7 +141,7 @@ const useRecoveryFactorValidation = (node: Node): void => {
}

return false;
}, [ isRecoveryFactorValidationEnabled, containsResetPassword, node, ancestors ]);
}, [ isRecoveryFactorValidationEnabled, containsResetPassword, node, ancestors, recoveryFactorExecutors ]);

const errorNotificationId: string = `recovery-factor-validation-${node?.id}`;

Expand Down Expand Up @@ -145,9 +170,8 @@ const useRecoveryFactorValidation = (node: Node): void => {
errorNotificationId,
t(
"flowBuilder:validations.passwordRecoveryRequiresFactors.message",
"Password recovery requires at least one of the following " +
"factors to be present in the flow: Email OTP, SMS OTP, " +
"or Magic Link."
"Password recovery requires at least one recovery factor, such as " +
"Email OTP, SMS OTP or Magic Link, to be present in the flow."
),
NotificationType.ERROR
);
Expand Down
Loading
Loading