diff --git a/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res b/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res
index d2c264667..64d6657c1 100644
--- a/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res
+++ b/src/entryPoints/AuthModule/PreLoginModule/DecisionScreen.res
@@ -18,7 +18,7 @@ let make = () => {
| MERCHANT_SELECT
| ACCEPT_INVITE =>
- | TOTP =>
+ | TOTP =>
| FORCE_SET_PASSWORD
| RESET_PASSWORD =>
diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res b/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res
index daf68f163..a07064f4f 100644
--- a/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res
+++ b/src/entryPoints/AuthModule/TwoFaAuth/TotpSetup.res
@@ -4,16 +4,15 @@ let p3Regular = HSwitchUtils.getTextClass((P3, Regular))
module EnterAccessCode = {
@react.component
- let make = (~setTwoFaPageState, ~onClickVerifyAccessCode) => {
+ let make = (~setTwoFaPageState, ~onClickVerifyAccessCode, ~errorHandling) => {
let showToast = ToastState.useShowToast()
let verifyRecoveryCodeLogic = TotpHooks.useVerifyRecoveryCode()
let (recoveryCode, setRecoveryCode) = React.useState(_ => "")
let (buttonState, setButtonState) = React.useState(_ => Button.Normal)
- let verifyAccessCode = async () => {
+ let verifyAccessCode = async _ => {
+ open LogicUtils
try {
- open LogicUtils
-
setButtonState(_ => Button.Loading)
if recoveryCode->String.length > 0 {
@@ -25,7 +24,12 @@ module EnterAccessCode = {
}
setButtonState(_ => Button.Normal)
} catch {
- | _ => {
+ | Exn.Error(e) => {
+ let err = Exn.message(e)->Option.getOr("Something went wrong")
+ let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "")
+ if errorCode == "UR_49" {
+ errorHandling(errorCode)
+ }
setRecoveryCode(_ => "")
setButtonState(_ => Button.Normal)
}
@@ -105,6 +109,7 @@ module ConfigureTotpScreen = {
~twoFaStatus,
~setTwoFaPageState,
~terminateTwoFactorAuth,
+ ~errorHandling,
) => {
open TwoFaTypes
@@ -115,9 +120,8 @@ module ConfigureTotpScreen = {
let (buttonState, setButtonState) = React.useState(_ => Button.Normal)
let verifyTOTP = async () => {
+ open LogicUtils
try {
- open LogicUtils
-
setButtonState(_ => Button.Loading)
if otp->String.length > 0 {
@@ -135,7 +139,12 @@ module ConfigureTotpScreen = {
}
setButtonState(_ => Button.Normal)
} catch {
- | _ => {
+ | Exn.Error(e) => {
+ let err = Exn.message(e)->Option.getOr("Something went wrong")
+ let errorCode = err->safeParse->getDictFromJsonObject->getString("code", "")
+ if errorCode == "UR_48" {
+ errorHandling(errorCode)
+ }
setOtp(_ => "")
setButtonState(_ => Button.Normal)
}
@@ -225,7 +234,7 @@ module ConfigureTotpScreen = {
}
@react.component
-let make = () => {
+let make = (~setTwoFaPageState, ~twoFaPageState, ~errorHandling) => {
open HSwitchUtils
open TwoFaTypes
@@ -238,7 +247,6 @@ let make = () => {
let (isQrVisible, setIsQrVisible) = React.useState(_ => false)
let (totpUrl, setTotpUrl) = React.useState(_ => "")
let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TWO_FA_NOT_SET)
- let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR)
let (showNewQR, setShowNewQR) = React.useState(_ => false)
let delayTimer = () => {
@@ -325,14 +333,16 @@ let make = () => {
{switch twoFaPageState {
| TOTP_SHOW_QR =>
| TOTP_SHOW_RC =>
| TOTP_INPUT_RECOVERY_CODE =>
-
+
}}
{"Log in with a different account?"->React.string}
diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res
new file mode 100644
index 000000000..0c384863e
--- /dev/null
+++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaLanding.res
@@ -0,0 +1,126 @@
+let p2Regular = HSwitchUtils.getTextClass((P2, Regular))
+
+module AttemptsExpiredComponent = {
+ @react.component
+ let make = (~expiredType, ~setTwoFaPageState, ~setTwoFaStatus) => {
+ open TwoFaTypes
+ open HSwitchUtils
+ let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext)
+
+ let belowComponent = switch expiredType {
+ | TOTP_ATTEMPTS_EXPIRED =>
+
+ {"or "->React.string}
+ {
+ setTwoFaStatus(_ => TwoFaNotExpired)
+ setTwoFaPageState(_ => TOTP_INPUT_RECOVERY_CODE)
+ }}>
+ {"Use recovery-code"->React.string}
+
+
+ | RC_ATTEMPTS_EXPIRED =>
+
+ {"or "->React.string}
+ {
+ setTwoFaStatus(_ => TwoFaNotExpired)
+ setTwoFaPageState(_ => TOTP_SHOW_QR)
+ }}>
+ {"Use totp"->React.string}
+
+
+ | TWO_FA_EXPIRED => React.null
+ }
+
+
+
+
+
+ {"There have been multiple unsuccessful sign-in attempts for this account. Please wait a moment before trying again."->React.string}
+
+ {belowComponent}
+
+
+ {"Log in with a different account?"->React.string}
+
setAuthStatus(LoggedOut)}>
+ {"Click here to log out."->React.string}
+
+
+
+
+ }
+}
+
+@react.component
+let make = () => {
+ open TwoFaTypes
+ let getURL = APIUtils.useGetURL()
+ let fetchDetails = APIUtils.useGetMethod()
+ let {setAuthStatus} = React.useContext(AuthInfoProvider.authStatusContext)
+ let (twoFaStatus, setTwoFaStatus) = React.useState(_ => TwoFaNotExpired)
+ let (twoFaPageState, setTwoFaPageState) = React.useState(_ => TOTP_SHOW_QR)
+ let (screenState, setScreenState) = React.useState(_ => PageLoaderWrapper.Loading)
+
+ let handlePageBasedOnAttempts = responseDict => {
+ switch responseDict {
+ | Some(value) =>
+ if value.totp.attemptsRemaining > 0 && value.recoveryCode.attemptsRemaining > 0 {
+ setTwoFaStatus(_ => TwoFaNotExpired)
+ setTwoFaPageState(_ => TOTP_SHOW_QR)
+ } else if value.totp.attemptsRemaining == 0 && value.recoveryCode.attemptsRemaining == 0 {
+ setTwoFaStatus(_ => TwoFaExpired(TWO_FA_EXPIRED))
+ } else if value.totp.attemptsRemaining == 0 {
+ setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED))
+ } else if value.recoveryCode.attemptsRemaining == 0 {
+ setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED))
+ }
+ | None => setTwoFaStatus(_ => TwoFaNotExpired)
+ }
+ }
+
+ let checkTwofaStatus = async () => {
+ try {
+ let url = getURL(
+ ~entityName=USERS,
+ ~userType=#CHECK_TWO_FACTOR_AUTH_STATUS_V2,
+ ~methodType=Get,
+ )
+ let response = await fetchDetails(url)
+ let responseDict = response->TwoFaUtils.jsonTocheckTwofaResponseType
+ handlePageBasedOnAttempts(responseDict.status)
+ setScreenState(_ => PageLoaderWrapper.Success)
+ } catch {
+ | _ => {
+ setScreenState(_ => PageLoaderWrapper.Error("Failed to fetch!"))
+ setAuthStatus(LoggedOut)
+ }
+ }
+ }
+
+ let errorHandling = errorCode => {
+ if errorCode == "UR_48" {
+ setTwoFaStatus(_ => TwoFaExpired(TOTP_ATTEMPTS_EXPIRED))
+ } else if errorCode == "UR_49" {
+ setTwoFaStatus(_ => TwoFaExpired(RC_ATTEMPTS_EXPIRED))
+ }
+ }
+
+ React.useEffect(() => {
+ checkTwofaStatus()->ignore
+ None
+ }, [])
+
+
+ {switch twoFaStatus {
+ | TwoFaExpired(expiredType) =>
+
+ | TwoFaNotExpired =>
+ }}
+
+}
diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res
index 289485444..fad0c7f59 100644
--- a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res
+++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaTypes.res
@@ -1,3 +1,25 @@
-type twoFaPageState = TOTP_SHOW_QR | TOTP_SHOW_RC | TOTP_INPUT_RECOVERY_CODE
+type twoFaPageState =
+ | TOTP_SHOW_QR
+ | TOTP_SHOW_RC
+ | TOTP_INPUT_RECOVERY_CODE
type twoFaStatus = TWO_FA_NOT_SET | TWO_FA_SET
+
+type twoFaValueType = {
+ isCompleted: bool,
+ attemptsRemaining: int,
+}
+
+type twoFatype = {
+ totp: twoFaValueType,
+ recoveryCode: twoFaValueType,
+}
+
+type checkTwofaResponseType = {status: option
}
+
+type expiredTypes =
+ | TOTP_ATTEMPTS_EXPIRED
+ | RC_ATTEMPTS_EXPIRED
+ | TWO_FA_EXPIRED
+
+type twoFaStatusType = TwoFaExpired(expiredTypes) | TwoFaNotExpired
diff --git a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res
index 3832ceac7..8c5e06e8b 100644
--- a/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res
+++ b/src/entryPoints/AuthModule/TwoFaAuth/TwoFaUtils.res
@@ -63,3 +63,33 @@ let downloadRecoveryCodes = (~recoveryCodes) => {
~content=JSON.stringifyWithIndent(recoveryCodes->getJsonFromArrayOfString, 3),
)
}
+
+let jsonToTwoFaValueType: Dict.t<'a> => TwoFaTypes.twoFaValueType = dict => {
+ open LogicUtils
+
+ {
+ isCompleted: dict->getBool("is_completed", false),
+ attemptsRemaining: dict->getInt("remaining_attempts", 4),
+ }
+}
+
+let jsonTocheckTwofaResponseType: JSON.t => TwoFaTypes.checkTwofaResponseType = json => {
+ open LogicUtils
+ let jsonToDict = json->getDictFromJsonObject->Dict.get("status")
+
+ let statusValue = switch jsonToDict {
+ | Some(json) => {
+ let dict = json->getDictFromJsonObject
+ let twoFaValue: TwoFaTypes.twoFatype = {
+ totp: dict->getDictfromDict("totp")->jsonToTwoFaValueType,
+ recoveryCode: dict->getDictfromDict("recovery_code")->jsonToTwoFaValueType,
+ }
+ Some(twoFaValue)
+ }
+ | None => None
+ }
+
+ {
+ status: statusValue,
+ }
+}
diff --git a/src/screens/APIUtils/APIUtils.res b/src/screens/APIUtils/APIUtils.res
index 6888eb4de..12f79baeb 100644
--- a/src/screens/APIUtils/APIUtils.res
+++ b/src/screens/APIUtils/APIUtils.res
@@ -661,6 +661,7 @@ let useGetURL = () => {
// SPT FLOWS (Totp)
| #BEGIN_TOTP => `${userUrl}/2fa/totp/begin`
+ | #CHECK_TWO_FACTOR_AUTH_STATUS_V2 => `${userUrl}/2fa/v2`
| #VERIFY_TOTP => `${userUrl}/2fa/totp/verify`
| #VERIFY_RECOVERY_CODE => `${userUrl}/2fa/recovery_code/verify`
| #GENERATE_RECOVERY_CODES => `${userUrl}/2fa/recovery_code/generate`
diff --git a/src/screens/APIUtils/APIUtilsTypes.res b/src/screens/APIUtils/APIUtilsTypes.res
index aaa908515..82a6d2ca4 100644
--- a/src/screens/APIUtils/APIUtilsTypes.res
+++ b/src/screens/APIUtils/APIUtilsTypes.res
@@ -114,6 +114,7 @@ type userType = [
| #USER_DETAILS
| #LIST_ROLES_FOR_ROLE_UPDATE
| #ACCEPT_INVITATION_HOME
+ | #CHECK_TWO_FACTOR_AUTH_STATUS_V2
| #NONE
]