diff --git a/.changeset/new-cases-listen.md b/.changeset/new-cases-listen.md new file mode 100644 index 00000000..5cbd4ba6 --- /dev/null +++ b/.changeset/new-cases-listen.md @@ -0,0 +1,8 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/react': patch +'@asgardeo/i18n': patch +--- + +- Fix `SignUp` component issues with `AsgardeoV2` +- Fix `Password` field display issue in `SignIn` component diff --git a/packages/i18n/src/models/i18n.ts b/packages/i18n/src/models/i18n.ts index 7fa72be4..5052fb5f 100644 --- a/packages/i18n/src/models/i18n.ts +++ b/packages/i18n/src/models/i18n.ts @@ -42,6 +42,9 @@ export interface I18nTranslations { 'elements.fields.username': string; 'elements.fields.password': string; + /* Validation */ + 'field.required': string; + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -143,6 +146,11 @@ export interface I18nTranslations { 'errors.sign.in.flow.completion.failure': string; 'errors.sign.in.flow.passkeys.failure': string; 'errors.sign.in.flow.passkeys.completion.failure': string; + 'errors.sign.up.initialization': string; + 'errors.sign.up.flow.failure': string; + 'errors.sign.up.flow.initialization.failure': string; + 'errors.sign.up.components.not.available': string; + 'errors.sign.in.components.not.available': string; } export type I18nTextDirection = 'ltr' | 'rtl'; diff --git a/packages/i18n/src/translations/en-US.ts b/packages/i18n/src/translations/en-US.ts index 74a139bd..cda31c64 100644 --- a/packages/i18n/src/translations/en-US.ts +++ b/packages/i18n/src/translations/en-US.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'Username', 'elements.fields.password': 'Password', + /* Validation */ + 'field.required': 'This field is required', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -144,6 +147,12 @@ const translations: I18nTranslations = { 'errors.sign.in.flow.passkeys.failure': 'An error occurred while signing in with passkeys. Please try again later.', 'errors.sign.in.flow.passkeys.completion.failure': 'An error occurred while completing the passkeys sign-in flow. Please try again later.', + 'errors.sign.up.initialization': 'An error occurred while initializing. Please try again later.', + 'errors.sign.up.flow.failure': 'An error occurred during the sign-up flow. Please try again later.', + 'errors.sign.up.flow.initialization.failure': + 'An error occurred while initializing the sign-up flow. Please try again later.', + 'errors.sign.up.components.not.available': 'Sign-up form is not available at the moment. Please try again later.', + 'errors.sign.in.components.not.available': 'Sign-in form is not available at the moment. Please try again later.', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/fr-FR.ts b/packages/i18n/src/translations/fr-FR.ts index fac146a2..7edce75f 100644 --- a/packages/i18n/src/translations/fr-FR.ts +++ b/packages/i18n/src/translations/fr-FR.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': "Nom d'utilisateur", 'elements.fields.password': 'Mot de passe', + /* Validation */ + 'field.required': 'Ce champ est obligatoire', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -146,6 +149,14 @@ const translations: I18nTranslations = { "Une erreur est survenue lors de la connexion avec les clefs d'accès. Veuillez réessayer plus tard.", 'errors.sign.in.flow.passkeys.completion.failure': "Une erreur est survenue lors de la finalisation du flux de connexion avec les clefs d'accès. Veuillez réessayer plus tard.", + 'errors.sign.up.initialization': "Une erreur est survenue lors de l'initialisation. Veuillez réessayer plus tard.", + 'errors.sign.up.flow.failure': "Une erreur est survenue lors du flux d'inscription. Veuillez réessayer plus tard.", + 'errors.sign.up.flow.initialization.failure': + "Une erreur est survenue lors de l'initialisation du flux d'inscription. Veuillez réessayer plus tard.", + 'errors.sign.up.components.not.available': + "Le formulaire d'inscription n'est pas disponible pour le moment. Veuillez réessayer plus tard.", + 'errors.sign.in.components.not.available': + "Le formulaire de connexion n'est pas disponible pour le moment. Veuillez réessayer plus tard.", }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/hi-IN.ts b/packages/i18n/src/translations/hi-IN.ts index cdd7dab2..c74abd99 100644 --- a/packages/i18n/src/translations/hi-IN.ts +++ b/packages/i18n/src/translations/hi-IN.ts @@ -44,6 +44,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'उपयोगकर्ता नाम', 'elements.fields.password': 'पासवर्ड', + /* Validation */ + 'field.required': 'यह फील्ड आवश्यक है', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -141,6 +144,12 @@ const translations: I18nTranslations = { 'errors.sign.in.flow.completion.failure': 'साइन-इन प्रक्रिया पूरी करते समय त्रुटि। कृपया बाद में पुनः प्रयास करें।', 'errors.sign.in.flow.passkeys.failure': 'पासकीज़ के साथ साइन-इन करते समय त्रुटि।', 'errors.sign.in.flow.passkeys.completion.failure': 'पासकीज़ साइन-इन पूरी करते समय त्रुटि।', + 'errors.sign.up.initialization': 'प्रारंभीकरण के दौरान एक त्रुटि हुई। कृपया बाद में पुनः प्रयास करें।', + 'errors.sign.up.flow.failure': 'साइन-अप प्रक्रिया में त्रुटि। कृपया बाद में पुनः प्रयास करें।', + 'errors.sign.up.flow.initialization.failure': + 'साइन-अप प्रक्रिया प्रारंभ करते समय त्रुटि। कृपया बाद में पुनः प्रयास करें।', + 'errors.sign.up.components.not.available': 'साइन-अप फॉर्म फिलहाल उपलब्ध नहीं है। कृपया बाद में पुनः प्रयास करें।', + 'errors.sign.in.components.not.available': 'साइन-इन फॉर्म फिलहाल उपलब्ध नहीं है। कृपया बाद में पुनः प्रयास करें।', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/ja-JP.ts b/packages/i18n/src/translations/ja-JP.ts index ed3c97e1..6ed5aaa0 100644 --- a/packages/i18n/src/translations/ja-JP.ts +++ b/packages/i18n/src/translations/ja-JP.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'ユーザー名', 'elements.fields.password': 'パスワード', + /* Validation */ + 'field.required': 'この項目は必須です', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -141,9 +144,16 @@ const translations: I18nTranslations = { 'errors.sign.in.flow.failure': 'サインイン処理中にエラーが発生しました。後でもう一度お試しください。', 'errors.sign.in.flow.completion.failure': 'サインイン処理の完了中にエラーが発生しました。後でもう一度お試しください。', - 'errors.sign.in.flow.passkeys.failure': 'パスキーでのサインイン中にエラーが発生しました。後でもう一度お試しください。', + 'errors.sign.in.flow.passkeys.failure': + 'パスキーでのサインイン中にエラーが発生しました。後でもう一度お試しください。', 'errors.sign.in.flow.passkeys.completion.failure': 'パスキーによるサインイン完了中にエラーが発生しました。後でもう一度お試しください。', + 'errors.sign.up.initialization': '初期化中にエラーが発生しました。後でもう一度お試しください。', + 'errors.sign.up.flow.failure': 'サインアップフロー中にエラーが発生しました。後でもう一度お試しください。', + 'errors.sign.up.flow.initialization.failure': + 'サインアップフローの初期化中にエラーが発生しました。後でもう一度お試しください。', + 'errors.sign.up.components.not.available': 'サインアップフォームは現在利用できません。後でもう一度お試しください。', + 'errors.sign.in.components.not.available': 'サインインフォームは現在利用できません。後でもう一度お試しください。', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/pt-BR.ts b/packages/i18n/src/translations/pt-BR.ts index a4003971..7e64a5d4 100644 --- a/packages/i18n/src/translations/pt-BR.ts +++ b/packages/i18n/src/translations/pt-BR.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'Nome de usuário', 'elements.fields.password': 'Senha', + /* Validation */ + 'field.required': 'Este campo é obrigatório', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -144,6 +147,14 @@ const translations: I18nTranslations = { 'Ocorreu um erro ao entrar com as chaves de acesso (passkeys). Tente novamente mais tarde.', 'errors.sign.in.flow.passkeys.completion.failure': 'Ocorreu um erro ao completar o login com as chaves de acesso (passkeys). Tente novamente mais tarde.', + 'errors.sign.up.initialization': 'Ocorreu um erro durante a inicialização. Tente novamente mais tarde.', + 'errors.sign.up.flow.failure': 'Ocorreu um erro durante o fluxo de cadastro. Tente novamente mais tarde.', + 'errors.sign.up.flow.initialization.failure': + 'Ocorreu um erro ao inicializar o fluxo de cadastro. Tente novamente mais tarde.', + 'errors.sign.up.components.not.available': + 'O formulário de cadastro não está disponível no momento. Tente novamente mais tarde.', + 'errors.sign.in.components.not.available': + 'O formulário de login não está disponível no momento. Tente novamente mais tarde.', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/pt-PT.ts b/packages/i18n/src/translations/pt-PT.ts index f66fad9b..c4e885cd 100644 --- a/packages/i18n/src/translations/pt-PT.ts +++ b/packages/i18n/src/translations/pt-PT.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'Nome de utilizador', 'elements.fields.password': 'Palavra-passe', + /* Validation */ + 'field.required': 'Este campo é obrigatório', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -145,6 +148,14 @@ const translations: I18nTranslations = { 'Ocorreu um erro ao iniciar sessão com as chaves de acesso (passkeys). Tente novamente mais tarde.', 'errors.sign.in.flow.passkeys.completion.failure': 'Ocorreu um erro ao completar o início de sessão com as chaves de acesso (passkeys). Tente novamente mais tarde.', + 'errors.sign.up.initialization': 'Ocorreu um erro durante a inicialização. Tente novamente mais tarde.', + 'errors.sign.up.flow.failure': 'Ocorreu um erro durante o fluxo de registo. Tente novamente mais tarde.', + 'errors.sign.up.flow.initialization.failure': + 'Ocorreu um erro ao inicializar o fluxo de registo. Tente novamente mais tarde.', + 'errors.sign.up.components.not.available': + 'O formulário de registo não está disponível de momento. Tente novamente mais tarde.', + 'errors.sign.in.components.not.available': + 'O formulário de início de sessão não está disponível de momento. Tente novamente mais tarde.', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/si-LK.ts b/packages/i18n/src/translations/si-LK.ts index 750e6726..9f6bd7b3 100644 --- a/packages/i18n/src/translations/si-LK.ts +++ b/packages/i18n/src/translations/si-LK.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'පරිශීලක නාමය', 'elements.fields.password': 'මුරපදය', + /* Validation */ + 'field.required': 'මෙම ක්ෂේත්‍රය අවශ්‍යයි', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -144,6 +147,12 @@ const translations: I18nTranslations = { 'errors.sign.in.flow.passkeys.failure': 'passkeys සමඟ ලොග් වීමේදී දෝෂයක් සිදු විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', 'errors.sign.in.flow.passkeys.completion.failure': 'passkeys සමඟ ලොග් වීමේ ක්‍රියාවලිය සම්පූර්ණ කිරීමේදී දෝෂයක් සිදු විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', + 'errors.sign.up.initialization': 'ආරම්භ කිරීමේදී දෝෂයක් සිදු විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', + 'errors.sign.up.flow.failure': 'ගිණුම් තැනීමේ ක්‍රියාවලියේදී දෝෂයක් සිදු විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', + 'errors.sign.up.flow.initialization.failure': + 'ගිණුම් තැනීමේ ක්‍රියාවලිය ආරම්භ කිරීමේදී දෝෂයක් සිදු විය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', + 'errors.sign.up.components.not.available': 'ගිණුම් තැනීමේ පිටුව දැන් ලබා ගත නොහැකිය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', + 'errors.sign.in.components.not.available': 'ප්‍රවේශ වීමේ පිටුව දැන් ලබා ගත නොහැකිය. කරුණාකර පසුව නැවත උත්සාහ කරන්න.', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/ta-IN.ts b/packages/i18n/src/translations/ta-IN.ts index 28c1dcf6..705530e7 100644 --- a/packages/i18n/src/translations/ta-IN.ts +++ b/packages/i18n/src/translations/ta-IN.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'பயனர்பெயர்', 'elements.fields.password': 'கடவுச்சொல்', + /* Validation */ + 'field.required': 'இந்த புலம் தேவை', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -146,6 +149,12 @@ const translations: I18nTranslations = { 'பாஸ்கீக்கள் மூலம் உள்நுழையும்போது பிழை ஏற்பட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்.', 'errors.sign.in.flow.passkeys.completion.failure': 'பாஸ்கீ உள்நுழைவு முடிக்கும் போது பிழை ஏற்பட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்.', + 'errors.sign.up.initialization': 'தொடங்கும்போது பிழை ஏற்பட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்.', + 'errors.sign.up.flow.failure': 'பதிவு செய்யும் செயல்பாட்டில் பிழை ஏற்பட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்.', + 'errors.sign.up.flow.initialization.failure': + 'பதிவு செய்யும் செயல்பாட்டை தொடங்கும்போது பிழை ஏற்பட்டது. பின்னர் மீண்டும் முயற்சிக்கவும்.', + 'errors.sign.up.components.not.available': 'பதிவு படிவம் இப்போது கிடைக்கவில்லை. பின்னர் மீண்டும் முயற்சிக்கவும்.', + 'errors.sign.in.components.not.available': 'உள்நுழைவு படிவம் இப்போது கிடைக்கவில்லை. பின்னர் மீண்டும் முயற்சிக்கவும்.', }; const metadata: I18nMetadata = { diff --git a/packages/i18n/src/translations/te-IN.ts b/packages/i18n/src/translations/te-IN.ts index c6b81eb6..bdbebe1e 100644 --- a/packages/i18n/src/translations/te-IN.ts +++ b/packages/i18n/src/translations/te-IN.ts @@ -45,6 +45,9 @@ const translations: I18nTranslations = { 'elements.fields.username': 'వినియోగదారు పేరు', 'elements.fields.password': 'పాస్వర్డ్', + /* Validation */ + 'field.required': 'ఈ ఫీల్డ్ అవసరం', + /* |---------------------------------------------------------------| */ /* | Widgets | */ /* |---------------------------------------------------------------| */ @@ -140,8 +143,18 @@ const translations: I18nTranslations = { 'errors.sign.in.initialization': 'ప్రారంభించేటప్పుడు లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', 'errors.sign.in.flow.failure': 'సైన్ ఇన్ ప్రక్రియలో లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', 'errors.sign.in.flow.completion.failure': 'సైన్ ఇన్ పూర్తి చేయడంలో లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', - 'errors.sign.in.flow.passkeys.failure': 'పాస్‌కీలతో సైన్ ఇన్ చేస్తూ లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', - 'errors.sign.in.flow.passkeys.completion.failure': 'పాస్‌కీ సైన్ ఇన్ పూర్తి చేయడంలో లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.in.flow.passkeys.failure': + 'పాస్‌కీలతో సైన్ ఇన్ చేస్తూ లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.in.flow.passkeys.completion.failure': + 'పాస్‌కీ సైన్ ఇన్ పూర్తి చేయడంలో లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.up.initialization': 'ప్రారంభించేటప్పుడు లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.up.flow.failure': 'సైన్ అప్ ప్రక్రియలో లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.up.flow.initialization.failure': + 'సైన్ అప్ ప్రక్రియను ప్రారంభించేటప్పుడు లోపం వచ్చింది. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.up.components.not.available': + 'సైన్ అప్ ఫారం ప్రస్తుతం అందుబాటులో లేదు. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', + 'errors.sign.in.components.not.available': + 'సైన్ ఇన్ ఫారం ప్రస్తుతం అందుబాటులో లేదు. దయచేసి తరువాత మళ్లీ ప్రయత్నించండి.', }; const metadata: I18nMetadata = { diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index e92a4b48..8d004527 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -90,6 +90,7 @@ export { EmbeddedFlowComponentType, EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteRequestConfig, + EmbeddedFlowExecuteErrorResponse, } from './models/embedded-flow'; export { EmbeddedSignUpFlowStatusV2, @@ -100,6 +101,7 @@ export { ExtendedEmbeddedSignUpFlowResponseV2, EmbeddedSignUpFlowTypeV2, EmbeddedFlowExecuteRequestConfigV2, + EmbeddedSignUpFlowErrorResponseV2, } from './models/v2/embedded-signup-flow-v2'; export {FlowMode} from './models/flow'; export {AsgardeoClient} from './models/client'; diff --git a/packages/javascript/src/models/embedded-flow.ts b/packages/javascript/src/models/embedded-flow.ts index 07495b87..949cba19 100644 --- a/packages/javascript/src/models/embedded-flow.ts +++ b/packages/javascript/src/models/embedded-flow.ts @@ -69,8 +69,110 @@ export enum EmbeddedFlowComponentType { Typography = 'TYPOGRAPHY', } +/** + * Request configuration for executing embedded flow operations. + * + * This interface extends standard HTTP request configuration with additional + * properties specific to embedded flow execution, such as base URL and payload data. + * + * @template T - Type of the payload data being sent with the request + */ export interface EmbeddedFlowExecuteRequestConfig extends Partial { + /** + * Base URL for the API endpoint. + * This is typically the Asgardeo organization URL. + */ baseUrl?: string; + + /** + * Payload data to be sent with the request. + * The structure depends on the specific flow operation being executed. + */ payload?: T; + + /** + * Full URL for the API endpoint. + * If provided, this overrides the baseUrl. + */ url?: string; } + +/** + * Error response structure for AsgardeoV1 embedded flow operations. + * + * This interface defines the structure of error responses returned by AsgardeoV1 APIs + * when flow operations (such as sign-up or sign-in) fail. This format is distinct from + * AsgardeoV2's error format which uses `failureReason` instead of `code`/`description`. + * + * **Key Characteristics:** + * - Uses structured error codes (e.g., "FEE-60005") for programmatic error handling + * - Provides both a brief `message` and detailed `description` for context + * - Includes `flowType` to identify which flow operation failed + * + * **Comparison with AsgardeoV2:** + * - **AsgardeoV1**: Uses `code`, `message`, `description` fields + * - **AsgardeoV2**: Uses `flowStatus: "ERROR"` with `failureReason` field + * + * **Error Handling:** + * This error response format is automatically detected and processed by the + * `extractErrorMessage()` and `checkForErrorResponse()` functions in the React + * transformer to extract meaningful error messages for display to users. + * + * @example + * ```typescript + * // Typical AsgardeoV1 error response + * const errorResponse: EmbeddedFlowExecuteErrorResponse = { + * code: "FEE-60005", + * message: "Error while provisioning user.", + * description: "Error occurred while provisioning user in the request of flow id: ac57315c-6ca6-49dc-8664-fcdcff354f46", + * flowType: "REGISTRATION" + * }; + * + * // The transformer will extract: "Error occurred while provisioning user in the request of flow id: ac57315c-6ca6-49dc-8664-fcdcff354f46" + * // (Prefers description over message as it's usually more detailed) + * ``` + * + * @see {@link EmbeddedSignUpFlowErrorResponseV2} for the AsgardeoV2 equivalent error structure + */ +export interface EmbeddedFlowExecuteErrorResponse { + /** + * Structured error code identifying the type of error. + * + * Format typically follows pattern like "FEE-XXXXX" where: + * - "FEE" indicates Flow Execution Error + * - XXXXX is a numeric identifier for the specific error type + * + * @example "FEE-60005" - User provisioning error + */ + code: string; + + /** + * Brief error message describing what went wrong. + * + * This is typically a short, high-level description of the error. + * For more detailed information, refer to the `description` field. + * + * @example "Error while provisioning user." + */ + message: string; + + /** + * Detailed error description with contextual information. + * + * This field usually contains more specific information about the error, + * including flow IDs, operation details, and other debugging context. + * The transformer prefers this field over `message` when extracting + * error messages for display to users. + * + * @example "Error occurred while provisioning user in the request of flow id: ac57315c-6ca6-49dc-8664-fcdcff354f46" + */ + description: string; + + /** + * Type of flow operation that encountered the error. + * + * Currently only supports 'REGISTRATION' but may be extended to + * include other flow types (e.g., 'LOGIN', 'PASSWORD_RESET') in the future. + */ + flowType: 'REGISTRATION'; +} diff --git a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts index b8df4a8e..fa2be8b7 100644 --- a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts @@ -18,9 +18,43 @@ import {EmbeddedFlowExecuteRequestConfig, EmbeddedFlowResponseType, EmbeddedFlowType} from '../embedded-flow'; +/** + * Status enumeration for AsgardeoV2 embedded sign-up flow responses. + * + * This enum defines the possible states of a sign-up flow operation, + * allowing client applications to determine the next appropriate action. + * + * @experimental Part of the new AsgardeoV2 API + */ export enum EmbeddedSignUpFlowStatusV2 { + /** + * Sign-up flow has completed successfully. + * + * When this status is returned, the user has successfully registered + * and the flow can proceed to redirection or completion handling. + * The response will typically contain redirect information. + */ Complete = 'COMPLETE', + + /** + * Sign-up flow is in progress and requires additional user input. + * + * This status indicates that more steps are needed to complete the + * sign-up process. The response will contain form components or + * actions that need to be presented to the user. + */ Incomplete = 'INCOMPLETE', + + /** + * Sign-up flow encountered an error and cannot proceed. + * + * When this status is returned, the response will be of type + * `EmbeddedSignUpFlowErrorResponseV2` and will contain a `failureReason` + * field with details about what went wrong. This triggers error + * handling in the React components to display user-friendly messages. + * + * @see {@link EmbeddedSignUpFlowErrorResponseV2} for error response structure + */ Error = 'ERROR', } @@ -43,17 +77,57 @@ export interface ExtendedEmbeddedSignUpFlowResponseV2 { /** * Response structure for the new Asgardeo V2 embedded sign-up flow. - * @experimental + * + * This interface defines the structure for successful sign-up flow responses + * from AsgardeoV2 APIs. For error responses, see `EmbeddedSignUpFlowErrorResponseV2`. + * + * **Flow States:** + * - `INCOMPLETE`: More user input required, `data` contains form components + * - `COMPLETE`: Sign-up finished, may contain redirect information + * - For `ERROR` status, a separate `EmbeddedSignUpFlowErrorResponseV2` structure is used + * + * **Component-Driven UI:** + * The `data.inputs` and `data.actions` are transformed by the React transformer + * into component-driven format for consistent UI rendering across different + * Asgardeo versions. + * + * @experimental Part of the new AsgardeoV2 API + * @see {@link EmbeddedSignUpFlowErrorResponseV2} for error response structure + * @see {@link EmbeddedSignUpFlowStatusV2} for available flow statuses */ export interface EmbeddedSignUpFlowResponseV2 extends ExtendedEmbeddedSignUpFlowResponseV2 { + /** + * Unique identifier for this sign-up flow instance. + */ flowId: string; + + /** + * Current status of the sign-up flow. + * Determines whether more input is needed or the flow is complete. + */ flowStatus: EmbeddedSignUpFlowStatusV2; + + /** + * Type of response, indicating the expected user interaction. + */ type: EmbeddedSignUpFlowTypeV2; + + /** + * Flow data containing form inputs and available actions. + * This is transformed to component-driven format by the React transformer. + */ data: { + /** + * Available actions the user can take (e.g., form submission, social sign-up). + */ actions?: { type: EmbeddedFlowResponseType; id: string; }[]; + + /** + * Input fields required for the current step of the sign-up flow. + */ inputs?: { name: string; type: string; @@ -96,3 +170,76 @@ export interface EmbeddedSignUpFlowRequestV2 extends Partial extends EmbeddedFlowExecuteRequestConfig { sessionDataKey?: string; } + +/** + * Error response structure for the new Asgardeo V2 embedded sign-up flow. + * + * This interface defines the structure of error responses returned by AsgardeoV2 APIs + * when sign-up operations fail. Unlike AsgardeoV1 which uses generic error codes and + * descriptions, AsgardeoV2 provides more specific failure reasons within the flow context. + * + * **Key Differences from AsgardeoV1:** + * - Uses `failureReason` instead of `message`/`description` for error details + * - Maintains flow context with `flowId` for tracking failed operations + * - Uses structured `flowStatus` enum instead of generic error codes + * - Provides empty `data` object for consistency with success responses + * + * **Error Handling:** + * This error response format is automatically detected and processed by the + * `extractErrorMessage()` and `checkForErrorResponse()` functions in the transformer + * to extract meaningful error messages for display to users. + * + * @example + * ```typescript + * // Typical AsgardeoV2 error response + * const errorResponse: EmbeddedSignUpFlowErrorResponseV2 = { + * flowId: "0ccfeaf9-18b3-43a5-bcc1-07d863dcb2c0", + * flowStatus: EmbeddedSignUpFlowStatusV2.Error, + * data: {}, + * failureReason: "User already exists with the provided username." + * }; + * + * // This will be automatically transformed to a user-friendly error message: + * // "User already exists with the provided username." + * ``` + * + * @experimental This is part of the new AsgardeoV2 API and may change in future versions + * @see {@link EmbeddedSignUpFlowStatusV2.Error} for the error status enum value + * @see {@link EmbeddedSignUpFlowResponseV2} for the corresponding success response structure + */ +export interface EmbeddedSignUpFlowErrorResponseV2 { + /** + * Unique identifier for the sign-up flow instance. + * This ID is used to track the flow state and correlate error responses + * with the specific sign-up attempt that failed. + */ + flowId: string; + + /** + * Status of the sign-up flow, which will be `EmbeddedSignUpFlowStatusV2.Error` + * for error responses. This field is used by error detection logic to + * identify failed flow responses. + */ + flowStatus: EmbeddedSignUpFlowStatusV2; + + /** + * Additional response data, typically empty for error responses. + * Maintained for structural consistency with successful flow responses + * which contain components, actions, and other flow data. + */ + data: Record; + + /** + * Human-readable explanation of why the sign-up operation failed. + * + * This field contains specific error details that can be directly displayed + * to users, such as: + * - "User already exists with the provided username." + * - "Invalid email address format." + * - "Password does not meet complexity requirements." + * + * Unlike generic error codes, this provides contextual information + * that helps users understand and resolve the issue. + */ + failureReason: string; +} diff --git a/packages/react/src/components/factories/FieldFactory.tsx b/packages/react/src/components/factories/FieldFactory.tsx index 0bf2c90d..4dbff13c 100644 --- a/packages/react/src/components/factories/FieldFactory.tsx +++ b/packages/react/src/components/factories/FieldFactory.tsx @@ -54,6 +54,10 @@ export interface FieldConfig { * Callback function when the field value changes. */ onChange: (value: string) => void; + /** + * Callback function when the field loses focus. + */ + onBlur?: () => void; /** * Whether the field is disabled. */ @@ -136,6 +140,7 @@ export const createField = (config: FieldConfig): ReactElement => { required, value, onChange, + onBlur, disabled = false, error, className, @@ -155,6 +160,7 @@ export const createField = (config: FieldConfig): ReactElement => { className, value, placeholder, + onBlur, }; switch (type) { diff --git a/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx new file mode 100644 index 00000000..42494895 --- /dev/null +++ b/packages/react/src/components/presentation/SignIn/BaseSignIn.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025, 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 { + EmbeddedSignInFlowInitiateResponse, + EmbeddedSignInFlowHandleResponse, + EmbeddedSignInFlowHandleRequestPayload, + Platform, +} from '@asgardeo/browser'; +import {FC, ReactElement} from 'react'; +import BaseSignInV1, {BaseSignInProps as BaseSignInV1Props} from './non-component-driven/BaseSignIn'; +import BaseSignInV2, {BaseSignInProps as BaseSignInV2Props} from './component-driven/BaseSignIn'; +import ComponentDrivenSignIn, {SignInRenderProps} from './component-driven/SignIn'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; + +/** + * Props for the BaseSignIn component. + * Extends BaseSignInV1Props & BaseSignInV2Props for full compatibility with both React BaseSignIn components. + */ +export type BaseSignInProps = BaseSignInV1Props | BaseSignInV2Props; + +const BaseSignIn: FC = (props: BaseSignInProps) => { + const {platform} = useAsgardeo(); + + if (platform === Platform.AsgardeoV2) { + return ; + } + + return ; +}; + +export default BaseSignIn; diff --git a/packages/react/src/components/presentation/SignIn/SignIn.tsx b/packages/react/src/components/presentation/SignIn/SignIn.tsx index ca5c2ee1..bc400ea4 100644 --- a/packages/react/src/components/presentation/SignIn/SignIn.tsx +++ b/packages/react/src/components/presentation/SignIn/SignIn.tsx @@ -23,15 +23,15 @@ import { Platform, } from '@asgardeo/browser'; import {FC, ReactElement} from 'react'; -import BaseSignIn, {BaseSignInProps} from './non-component-driven/BaseSignIn'; -import ComponentDrivenSignIn, {SignInRenderProps} from './component-driven/SignIn'; +import BaseSignInV1, {BaseSignInProps as BaseSignInV1Props} from './non-component-driven/BaseSignIn'; +import SignInV2, {SignInRenderProps} from './component-driven/SignIn'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props for the SignIn component. * Extends BaseSignInProps for full compatibility with the React BaseSignIn component */ -export type SignInProps = Pick & { +export type SignInProps = Pick & { /** * Render function for custom UI (render props pattern). */ @@ -102,7 +102,7 @@ const SignIn: FC = ({className, size = 'medium', children, ...rest} if (platform === Platform.AsgardeoV2) { return ( - = ({className, size = 'medium', children, ...rest} onError={rest.onError} > {children} - + ); } return ( - = ({ /** * Extract form fields from flow components */ - const extractFormFields = useCallback( + const extractFormFields: (components: EmbeddedFlowComponent[]) => FormField[] = useCallback( (components: EmbeddedFlowComponent[]): FormField[] => { const fields: FormField[] = []; @@ -281,7 +281,7 @@ const BaseSignInContent: FC = ({ const identifier: string = (component.config['identifier'] as string) || component.id; fields.push({ name: identifier, - required: component.config['required'] as unknown as boolean || false, + required: (component.config['required'] as unknown as boolean) || false, initialValue: '', validator: (value: string) => { if (component.config['required'] && (!value || value.trim() === '')) { @@ -303,9 +303,9 @@ const BaseSignInContent: FC = ({ [t], ); - const formFields = components ? extractFormFields(components) : []; + const formFields: FormField[] = components ? extractFormFields(components) : []; - const form = useForm>({ + const form: ReturnType = useForm>({ initialValues: {}, fields: formFields, validateOnBlur: true, @@ -327,15 +327,21 @@ const BaseSignInContent: FC = ({ /** * Handle input value changes. */ - const handleInputChange = (name: string, value: string) => { + const handleInputChange = (name: string, value: string): void => { setFormValue(name, value); + }; + + /** + * Handle input blur events (when field loses focus). + */ + const handleInputBlur = (name: string): void => { setFormTouched(name, true); }; /** * Handle component submission (for buttons and actions). */ - const handleSubmit = async (component: EmbeddedFlowComponent, data?: Record) => { + const handleSubmit = async (component: EmbeddedFlowComponent, data?: Record): Promise => { // Mark all fields as touched before validation touchAllFields(); @@ -431,6 +437,7 @@ const BaseSignInContent: FC = ({ buttonClassName: buttonClasses, error, inputClassName: inputClasses, + onInputBlur: handleInputBlur, onSubmit: handleSubmit, size, variant, @@ -447,6 +454,7 @@ const BaseSignInContent: FC = ({ error, inputClasses, buttonClasses, + handleInputBlur, handleSubmit, ], ); diff --git a/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx b/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx index ad24164b..76d70b83 100644 --- a/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignIn/component-driven/SignInOptionFactory.tsx @@ -74,6 +74,7 @@ const createSignInComponentFromFlow = ( error?: string | null; inputClassName?: string; key?: string | number; + onInputBlur?: (name: string) => void; onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; size?: 'small' | 'medium' | 'large'; variant?: any; @@ -83,7 +84,7 @@ const createSignInComponentFromFlow = ( switch (component.type) { case EmbeddedFlowComponentType.Input: { - const identifier: string = component.config['identifier'] as string || component.id; + const identifier: string = (component.config['identifier'] as string) || component.id; const value: string = formValues[identifier] || ''; const isTouched: boolean = touchedFields[identifier] || false; const error: string = isTouched ? formErrors[identifier] : undefined; @@ -92,12 +93,13 @@ const createSignInComponentFromFlow = ( const field = createField({ type: fieldType as FieldType, name: identifier, - label: component.config['label'] as string || '', - placeholder: component.config['placeholder'] as string || '', - required: component.config['required'] as unknown as boolean || false, + label: (component.config['label'] as string) || '', + placeholder: (component.config['placeholder'] as string) || '', + required: (component.config['required'] as unknown as boolean) || false, value, error, onChange: (newValue: string) => onInputChange(identifier, newValue), + onBlur: () => options.onInputBlur?.(identifier), className: options.inputClassName, }); @@ -144,7 +146,7 @@ const createSignInComponentFromFlow = ( variant={component.variant?.toLowerCase() === 'primary' ? 'solid' : 'outline'} color={component.variant?.toLowerCase() === 'primary' ? 'primary' : 'secondary'} > - {component.config['text'] as string || 'Submit'} + {(component.config['text'] as string) || 'Submit'} ); } @@ -153,7 +155,7 @@ const createSignInComponentFromFlow = ( const variant = getTypographyVariant(component.variant || 'H3'); return ( - {component.config['text'] as string || ''} + {(component.config['text'] as string) || ''} ); } @@ -216,6 +218,7 @@ export const renderSignInComponents = ( buttonClassName?: string; error?: string | null; inputClassName?: string; + onInputBlur?: (name: string) => void; onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; size?: 'small' | 'medium' | 'large'; variant?: any; diff --git a/packages/react/src/components/presentation/SignIn/component-driven/transformer.ts b/packages/react/src/components/presentation/SignIn/component-driven/transformer.ts index ca9b8acd..56e949a4 100644 --- a/packages/react/src/components/presentation/SignIn/component-driven/transformer.ts +++ b/packages/react/src/components/presentation/SignIn/component-driven/transformer.ts @@ -30,7 +30,17 @@ const generateId = (prefix: string): string => { /** * Convert simple input type to component variant */ -const getInputVariant = (type: string): 'TEXT' | 'EMAIL' | 'PASSWORD' => { +const getInputVariant = (type: string, name: string): 'TEXT' | 'EMAIL' | 'PASSWORD' => { + // Check name first (e.g., "password" field name) + const lowerName = name.toLowerCase(); + if (lowerName.includes('password')) { + return 'PASSWORD'; + } + if (lowerName.includes('email')) { + return 'EMAIL'; + } + + // Then check type switch (type.toLowerCase()) { case 'email': return 'EMAIL'; @@ -81,7 +91,7 @@ const convertSimpleInputToComponent = ( }, t: UseTranslation['t'], ): EmbeddedFlowComponent => { - const variant = getInputVariant(input.type); + const variant = getInputVariant(input.type, input.name); const label = getInputLabel(input.name, input.type, t); const placeholder = getInputPlaceholder(input.name, input.type, t); @@ -90,7 +100,7 @@ const convertSimpleInputToComponent = ( type: EmbeddedFlowComponentType.Input, variant, config: { - type: input.type === 'string' ? 'text' : input.type, + type: input.type, label, placeholder, required: input.required as boolean, diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index 467fd151..63e166c2 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -28,7 +28,7 @@ import { import {cx} from '@emotion/css'; import {FC, ReactElement, ReactNode, useEffect, useState, useCallback, useRef} from 'react'; import {renderSignUpComponents} from './SignUpOptionFactory'; -import {transformSimpleToComponentDriven} from './transformer'; +import {transformSimpleToComponentDriven, extractErrorMessage} from './transformer'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; import {useForm, FormField} from '../../../hooks/useForm'; @@ -38,6 +38,7 @@ import Alert from '../../primitives/Alert/Alert'; import Card, {CardProps} from '../../primitives/Card/Card'; import Logo from '../../primitives/Logo/Logo'; import Spinner from '../../primitives/Spinner/Spinner'; +import Typography from '../../primitives/Typography/Typography'; import useStyles from './BaseSignUp.styles'; /** @@ -69,11 +70,6 @@ export interface BaseSignUpRenderProps { */ isLoading: boolean; - /** - * Current error message - */ - error: string | null; - /** * Flow components */ @@ -293,13 +289,30 @@ const BaseSignUpContent: FC = ({ }) => { const {theme, colorScheme} = useTheme(); const {t} = useTranslation(); - const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow(); + const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow(); const styles = useStyles(theme, colorScheme); + /** + * Handle error responses and extract meaningful error messages + * Uses the transformer's extractErrorMessage function for consistency + */ + const handleError = useCallback( + (error: any) => { + const errorMessage: string = extractErrorMessage(error, t); + + // Clear existing messages and add the error message + clearMessages(); + addMessage({ + type: 'error', + message: errorMessage, + }); + }, + [t, addMessage, clearMessages], + ); + const [isLoading, setIsLoading] = useState(false); const [isFlowInitialized, setIsFlowInitialized] = useState(false); const [currentFlow, setCurrentFlow] = useState(null); - const [error, setError] = useState(null); const [formData, setFormData] = useState>({}); const initializationAttemptedRef = useRef(false); @@ -439,7 +452,7 @@ const BaseSignUpContent: FC = ({ } setIsLoading(true); - setError(null); + clearMessages(); try { // Filter out empty or undefined input values @@ -477,8 +490,7 @@ const BaseSignUpContent: FC = ({ setupFormFields(response); } } catch (err) { - const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.up.flow.failure'); - setError(errorMessage); + handleError(err); onError?.(err as Error); } finally { setIsLoading(false); @@ -550,8 +562,7 @@ const BaseSignUpContent: FC = ({ popup.close(); cleanup(); } catch (err) { - const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.up.flow.failure'); - setError(errorMessage); + handleError(err); onError?.(err as Error); popup.close(); cleanup(); @@ -629,8 +640,7 @@ const BaseSignUpContent: FC = ({ popup.close(); } catch (err) { - const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.up.flow.failure'); - setError(errorMessage); + handleError(err); onError?.(err as Error); popup.close(); } @@ -697,7 +707,6 @@ const BaseSignUpContent: FC = ({ handleInputChange, { buttonClassName: buttonClasses, - error, inputClassName: inputClasses, onSubmit: handleSubmit, size, @@ -712,7 +721,6 @@ const BaseSignUpContent: FC = ({ isLoading, size, variant, - error, inputClasses, buttonClasses, handleSubmit, @@ -726,7 +734,7 @@ const BaseSignUpContent: FC = ({ (async () => { setIsLoading(true); - setError(null); + clearMessages(); try { const rawResponse = await onInitialize(); @@ -746,8 +754,7 @@ const BaseSignUpContent: FC = ({ setupFormFields(response); } } catch (err) { - const errorMessage = err instanceof Error ? err.message : t('errors.sign.up.flow.initialization.failure'); - setError(errorMessage); + handleError(err); onError?.(err as Error); } finally { setIsLoading(false); @@ -775,7 +782,6 @@ const BaseSignUpContent: FC = ({ touched: touchedFields, isValid: isFormValid, isLoading, - error, components: currentFlow?.data?.components || [], handleInputChange, handleSubmit, @@ -805,8 +811,8 @@ const BaseSignUpContent: FC = ({ - {t('errors.title') || 'Error'} - {error || t('errors.sign.up.flow.initialization.failure')} + {t('errors.title')} + {t('errors.sign.up.flow.initialization.failure')} @@ -815,31 +821,37 @@ const BaseSignUpContent: FC = ({ return ( - {flowMessages && flowMessages.length > 0 && ( - -
+ + + {flowTitle || t('signup.title')} + + + {flowSubtitle || t('signup.subtitle')} + + {flowMessages && flowMessages.length > 0 && ( +
{flowMessages.map((message: any, index: number) => ( {message.message} ))}
-
- )} - - {error && ( - - {t('errors.title') || 'Error'} - {error} - )} + +
- {currentFlow.data?.components && renderComponents(currentFlow.data.components)} + {currentFlow.data?.components && currentFlow.data.components.length > 0 ? ( + renderComponents(currentFlow.data.components) + ) : ( + + {t('errors.sign.up.components.not.available')} + + )}
diff --git a/packages/react/src/components/presentation/SignUp/SignUpOptionFactory.tsx b/packages/react/src/components/presentation/SignUp/SignUpOptionFactory.tsx index a92fc528..6614ae94 100644 --- a/packages/react/src/components/presentation/SignUp/SignUpOptionFactory.tsx +++ b/packages/react/src/components/presentation/SignUp/SignUpOptionFactory.tsx @@ -51,11 +51,6 @@ export interface BaseSignUpOptionProps extends WithPreferences { */ component: EmbeddedFlowComponent; - /** - * Global error message to display. - */ - error?: string | null; - /** * Form validation errors. */ @@ -145,7 +140,7 @@ export const createSignUpComponent = ({component, onSubmit, ...rest}: BaseSignUp case EmbeddedFlowComponentType.Button: { const buttonVariant: string | undefined = component.variant?.toUpperCase(); - const buttonText: string = component.config['text'] as string || component.config['label'] as string || ''; + const buttonText: string = (component.config['text'] as string) || (component.config['label'] as string) || ''; // TODO: The connection type should come as metadata. if (buttonVariant === 'SOCIAL') { @@ -230,7 +225,6 @@ export const createSignUpOptionFromComponent = ( onInputChange: (name: string, value: string) => void, options?: { buttonClassName?: string; - error?: string | null; inputClassName?: string; key?: string | number; onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; @@ -262,7 +256,6 @@ export const renderSignUpComponents = ( onInputChange: (name: string, value: string) => void, options?: { buttonClassName?: string; - error?: string | null; inputClassName?: string; onSubmit?: (component: EmbeddedFlowComponent, data?: Record) => void; size?: 'small' | 'medium' | 'large'; diff --git a/packages/react/src/components/presentation/SignUp/transformer.ts b/packages/react/src/components/presentation/SignUp/transformer.ts index 1be17025..84cd8667 100644 --- a/packages/react/src/components/presentation/SignUp/transformer.ts +++ b/packages/react/src/components/presentation/SignUp/transformer.ts @@ -16,7 +16,12 @@ * under the License. */ -import {EmbeddedFlowComponent, EmbeddedFlowComponentType} from '@asgardeo/browser'; +import { + EmbeddedFlowComponent, + EmbeddedFlowComponentType, + EmbeddedSignUpFlowErrorResponseV2, + EmbeddedFlowExecuteErrorResponse, +} from '@asgardeo/browser'; import useTranslation, {UseTranslation} from '../../../hooks/useTranslation'; /** @@ -31,7 +36,17 @@ const generateId = (prefix: string): string => { /** * Convert simple input type to component variant */ -const getInputVariant = (type: string): 'TEXT' | 'EMAIL' | 'PASSWORD' => { +const getInputVariant = (type: string, name: string): 'TEXT' | 'EMAIL' | 'PASSWORD' => { + // Check name first (e.g., "password" field name) + const lowerName = name.toLowerCase(); + if (lowerName.includes('password')) { + return 'PASSWORD'; + } + if (lowerName.includes('email')) { + return 'EMAIL'; + } + + // Then check type switch (type.toLowerCase()) { case 'email': return 'EMAIL'; @@ -87,7 +102,7 @@ const convertSimpleInputToComponent = ( }, t: UseTranslation['t'], ): EmbeddedFlowComponent => { - const variant: 'TEXT' | 'EMAIL' | 'PASSWORD' = getInputVariant(input.type); + const variant: 'TEXT' | 'EMAIL' | 'PASSWORD' = getInputVariant(input.type, input.name); const label: string = getInputLabel(input.name, input.type, t); const placeholder: string = getInputPlaceholder(input.name, input.type, t); @@ -96,7 +111,7 @@ const convertSimpleInputToComponent = ( type: EmbeddedFlowComponentType.Input, variant, config: { - type: input.type === 'string' ? 'text' : input.type, + type: input.type, label, placeholder, required: input.required as boolean, @@ -189,8 +204,155 @@ export const transformSimpleToComponentDriven = (response: any, t: UseTranslatio return result; }; +/** + * Extract error message from various error response formats. + * + * This function supports multiple error formats from different versions of Asgardeo APIs: + * - **AsgardeoV2 format**: Uses `failureReason` field from responses with `flowStatus: "ERROR"` + * - **AsgardeoV1 format**: Uses `description` or `message` fields from responses with error `code` + * - **AsgardeoAPIError**: Parses JSON from error messages to extract detailed error information + * - **Standard Error objects**: Falls back to the `message` property + * - **String errors**: Returns the string as-is + * + * @param error - The error object, which can be: + * - AsgardeoV2 error response: `{ flowStatus: "ERROR", failureReason: string, ... }` + * - AsgardeoV1 error response: `{ code: string, message?: string, description?: string, ... }` + * - AsgardeoAPIError instance with JSON message + * - Standard Error object + * - String error message + * @param t - Translation function for fallback error messages + * @returns Localized error message extracted from the error object + * + * @example + * ```typescript + * // AsgardeoV2 error + * const v2Error = { flowStatus: "ERROR", failureReason: "User already exists" }; + * const message = extractErrorMessage(v2Error, t); // "User already exists" + * + * // AsgardeoV1 error + * const v1Error = { code: "FEE-60005", description: "Error while provisioning user" }; + * const message = extractErrorMessage(v1Error, t); // "Error while provisioning user" + * + * // AsgardeoAPIError + * const apiError = new AsgardeoAPIError('{"failureReason": "Invalid credentials"}'); + * const message = extractErrorMessage(apiError, t); // "Invalid credentials" + * ``` + */ +export const extractErrorMessage = ( + error: EmbeddedSignUpFlowErrorResponseV2 | EmbeddedFlowExecuteErrorResponse, + t: UseTranslation['t'], +): string => { + let errorMessage: string = t('errors.sign.up.flow.failure'); + + if (error && typeof error === 'object') { + // Handle AsgardeoV2 error format with failureReason + if ( + (error as EmbeddedSignUpFlowErrorResponseV2).flowStatus === 'ERROR' && + (error as EmbeddedSignUpFlowErrorResponseV2).failureReason + ) { + errorMessage = (error as EmbeddedSignUpFlowErrorResponseV2).failureReason; + } else if ( + (error as EmbeddedFlowExecuteErrorResponse).code && + ((error as EmbeddedFlowExecuteErrorResponse).message || (error as EmbeddedFlowExecuteErrorResponse).description) + ) { + errorMessage = + (error as EmbeddedFlowExecuteErrorResponse).description || (error as EmbeddedFlowExecuteErrorResponse).message; + } else if (error instanceof Error && error.name === 'AsgardeoAPIError') { + try { + const errorResponse: EmbeddedSignUpFlowErrorResponseV2 | EmbeddedFlowExecuteErrorResponse = JSON.parse( + error.message, + ); + + // Try AsgardeoV2 format first + if ((errorResponse as EmbeddedSignUpFlowErrorResponseV2).failureReason) { + errorMessage = (errorResponse as EmbeddedSignUpFlowErrorResponseV2).failureReason; + } + + // Try AsgardeoV1 format + else if ((errorResponse as EmbeddedFlowExecuteErrorResponse).description) { + errorMessage = (errorResponse as EmbeddedFlowExecuteErrorResponse).description; + } else if ((errorResponse as EmbeddedFlowExecuteErrorResponse).message) { + errorMessage = (errorResponse as EmbeddedFlowExecuteErrorResponse).message; + } else { + errorMessage = error.message; + } + } catch { + errorMessage = error.message; + } + } else if ( + ( + error as { + message: string; + } + ).message + ) { + errorMessage = ( + error as { + message: string; + } + ).message; + } + } else if (typeof error === 'string') { + errorMessage = error; + } + + return errorMessage; +}; + +/** + * Check if a response is an error response and extract the error message. + * + * This function serves as a guard to identify error responses from successful responses + * in both AsgardeoV1 and AsgardeoV2 API formats. It's particularly useful for flow + * normalization where error responses should be handled differently from success responses. + * + * **Supported Error Response Formats:** + * - **AsgardeoV2**: Responses with `flowStatus: "ERROR"` and `failureReason` + * - **AsgardeoV1**: Responses with error `code` and `message`/`description` fields + * + * @param response - The API response object to check for error indicators + * @param t - Translation function for extracting localized error messages + * @returns `null` if the response is not an error, or the extracted error message string if it is an error + * + * @example + * ```typescript + * // Success response + * const successResponse = { flowStatus: "INCOMPLETE", data: { components: [] } }; + * const error = checkForErrorResponse(successResponse, t); // null + * + * // AsgardeoV2 error response + * const v2ErrorResponse = { + * flowStatus: "ERROR", + * failureReason: "User already exists with the provided username." + * }; + * const error = checkForErrorResponse(v2ErrorResponse, t); // "User already exists with the provided username." + * + * // AsgardeoV1 error response + * const v1ErrorResponse = { + * code: "FEE-60005", + * message: "Error while provisioning user.", + * description: "Error occurred while provisioning user in the request of flow id: ac57315c-6ca6-49dc-8664-fcdcff354f46" + * }; + * const error = checkForErrorResponse(v1ErrorResponse, t); // "Error occurred while provisioning user in the request of flow id: ac57315c-6ca6-49dc-8664-fcdcff354f46" + * ``` + */ +export const checkForErrorResponse = (response: any, t: UseTranslation['t']): string | null => { + // Check for AsgardeoV2 error response format + if (response?.flowStatus === 'ERROR') { + return extractErrorMessage(response, t); + } + + // Check for AsgardeoV1 error response format + if (response?.code && (response?.message || response?.description)) { + return extractErrorMessage(response, t); + } + + return null; +}; + /** * Generic transformer that handles both simple and component-driven responses + * Also handles AsgardeoV2 error responses that should be thrown as errors */ export const normalizeFlowResponse = ( response: any, @@ -199,6 +361,14 @@ export const normalizeFlowResponse = ( flowId: string; components: EmbeddedFlowComponent[]; } => { + // Check if this is an error response and throw it as an error + // so it can be caught by the error handling in BaseSignUp + const errorMessage: string | null = checkForErrorResponse(response, t); + + if (errorMessage) { + throw response; + } + return { flowId: response.flowId, components: transformSimpleToComponentDriven(response, t), diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 64a9ff6d..d92da0b0 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -118,8 +118,8 @@ export * from './components/control/SignedOut'; export {default as Loading} from './components/control/Loading'; export * from './components/control/Loading'; -export {default as BaseSignIn} from './components/presentation/SignIn/non-component-driven/BaseSignIn'; -export * from './components/presentation/SignIn/non-component-driven/BaseSignIn'; +export {default as BaseSignIn} from './components/presentation/SignIn/BaseSignIn'; +export * from './components/presentation/SignIn/BaseSignIn'; export {default as SignIn} from './components/presentation/SignIn/SignIn'; export * from './components/presentation/SignIn/SignIn';