diff options
-rw-r--r-- | packages/challenger-ui/src/hooks/session.ts | 11 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AnswerChallenge.tsx | 111 | ||||
-rw-r--r-- | packages/challenger-ui/src/pages/AskChallenge.tsx | 56 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/challenger.ts | 4 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 40 |
5 files changed, 153 insertions, 69 deletions
diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts index 54eeb2fdc..4dc7e0dc1 100644 --- a/packages/challenger-ui/src/hooks/session.ts +++ b/packages/challenger-ui/src/hooks/session.ts @@ -41,7 +41,9 @@ export type SessionId = { }; export type LastChallengeResponse = { - attemptsLeft: number; + sendCodeLeft: number; + changeTargetLeft: number; + checkPinLeft: number; nextSend: AbsoluteTime; transmitted: boolean; }; @@ -53,7 +55,9 @@ export type SessionState = SessionId & { }; export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> => buildCodecForObject<LastChallengeResponse>() - .property("attemptsLeft", codecForNumber()) + .property("sendCodeLeft", codecForNumber()) + .property("changeTargetLeft", codecForNumber()) + .property("checkPinLeft", codecForNumber()) .property("nextSend", codecForAbsoluteTime) .property("transmitted", codecForBoolean()) .build("LastChallengeResponse"); @@ -127,7 +131,8 @@ export function useSessionState(): SessionStateHandler { const ls = state.lastStatus; if ( ls.changes_left !== st.changes_left || - ls.fix_address !== st.fix_address || ls.last_address !== st.last_address + ls.fix_address !== st.fix_address || + ls.last_address !== st.last_address ) { update({ ...state, diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx index b5b3b74b0..2740e1bdb 100644 --- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -17,6 +17,7 @@ import { AbsoluteTime, ChallengerApi, HttpStatusCode, + TalerProtocolTimestamp, assertUnreachable, } from "@gnu-taler/taler-util"; import { @@ -53,8 +54,7 @@ export function AnswerChallenge({ const { state, accepted, completed } = useSessionState(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [pin, setPin] = useState<string | undefined>(); - const [lastTryError, setLastTryError] = - useState<ChallengerApi.InvalidPinResponse>(); + const errors = undefinedIfEmpty({ pin: !pin ? i18n.str`Can't be empty` : undefined, }); @@ -68,7 +68,9 @@ export function AnswerChallenge({ : state.lastStatus.last_address["email"]; const onSendAgain = - !state || lastEmail === undefined + lastEmail === undefined || + state?.lastStatus == undefined || + state?.lastStatus.changes_left === 0 ? undefined : withErrorHandler( async () => { @@ -80,9 +82,11 @@ export function AnswerChallenge({ completed(new URL(ok.body.redirect_url)); } else { accepted({ - attemptsLeft: ok.body.attempts_left, + changeTargetLeft: ok.body.attempts_left, + checkPinLeft: state.lastStatus?.auth_attempts_left ?? 0, + sendCodeLeft: state.lastStatus?.pin_transmissions_left ?? 0, nextSend: AbsoluteTime.fromProtocolTimestamp( - ok.body.next_tx_time, + ok.body.retransmission_time, ), transmitted: ok.body.transmitted, }); @@ -92,23 +96,23 @@ export function AnswerChallenge({ (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str``; - case HttpStatusCode.Forbidden: - return i18n.str``; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; } }, ); const onCheck = - errors !== undefined || (lastTryError && lastTryError.exhausted) + errors !== undefined || + state?.lastStatus == undefined || + state?.lastStatus.auth_attempts_left === 0 ? undefined : withErrorHandler( async () => { @@ -118,25 +122,34 @@ export function AnswerChallenge({ if (ok.body.type === "completed") { completed(new URL(ok.body.redirect_url)); } else { - setLastTryError(ok.body); + accepted({ + changeTargetLeft: ok.body.addresses_left, + checkPinLeft: ok.body.auth_attempts_left, + sendCodeLeft: ok.body.pin_transmissions_left, + nextSend: AbsoluteTime.fromProtocolTimestamp( + state?.lastStatus?.retransmission_time ?? + TalerProtocolTimestamp.now(), + ), + transmitted: state?.lastTry?.transmitted ?? false, + }); } onComplete(); }, (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str`Invalid request`; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.Forbidden: { - return i18n.str`Too many attemps where made`; + return i18n.str`Invalid pin.`; } case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; default: assertUnreachable(fail); } @@ -177,11 +190,11 @@ export function AnswerChallenge({ </Attention> )} </p> - {!lastTryError ? undefined : ( + {!state.lastStatus ? undefined : ( <p class="mt-2 text-lg leading-8 text-gray-600"> <i18n.Translate> You can try another PIN but just{" "} - {lastTryError.auth_attempts_left} times more. + {state.lastStatus.auth_attempts_left} times more. </i18n.Translate> </p> )} @@ -225,8 +238,21 @@ export function AnswerChallenge({ <p class="mt-3 text-sm leading-6 text-gray-400"> <i18n.Translate> - You have {state.lastTry.attemptsLeft} attempts left. + We send the code {state.lastTry.checkPinLeft} more times. </i18n.Translate> + {state.lastTry.checkPinLeft < 1 ? ( + <i18n.Translate> + You can't check the PIN anymore. + </i18n.Translate> + ) : state.lastTry.checkPinLeft === 1 ? ( + <i18n.Translate> + You can check the PIN one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can check the PIN {state.lastTry.checkPinLeft} more times. + </i18n.Translate> + )} </p> </div> @@ -243,12 +269,31 @@ export function AnswerChallenge({ <div class="mt-10 flex justify-between"> <div> <a + data-disabled={!state.lastStatus || state.lastStatus.changes_left < 1} href={routeAsk.url({ nonce })} - class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" > <i18n.Translate>Change email</i18n.Translate> </a> - </div> + {state.lastStatus === undefined ? undefined : + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can't change the email anymore. + </i18n.Translate> + ) : state.lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the email one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the email {state.lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} + </p> + } + </div> <div> <Button type="submit" @@ -258,6 +303,22 @@ export function AnswerChallenge({ > <i18n.Translate>Send code again</i18n.Translate> </Button> + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastTry.sendCodeLeft < 1 ? ( + <i18n.Translate> + We can't send you the code anymore. + </i18n.Translate> + ) : state.lastTry.sendCodeLeft === 1 ? ( + <i18n.Translate> + We can send the code one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + We can send the code {state.lastTry.sendCodeLeft} more + times. + </i18n.Translate> + )} + </p> </div> </div> </form> diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx index cb0223d90..dc60562b7 100644 --- a/packages/challenger-ui/src/pages/AskChallenge.tsx +++ b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -48,13 +48,15 @@ export function AskChallenge({ focus, }: Props): VNode { const { state, accepted, completed } = useSessionState(); + const { lib, config } = useChallengerApiContext(); + const status = state?.lastStatus; const prevEmail = !status || !status.last_address ? undefined : status.last_address["email"]; - const regexEmail = - !status || !status.restrictions ? undefined : status.restrictions["email"]; + const regexEmail = !config.restrictions + ? undefined + : config.restrictions["email"]; - const { lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [email, setEmail] = useState<string | undefined>(); @@ -91,8 +93,12 @@ export function AskChallenge({ completed(new URL(ok.body.redirect_url)); } else { accepted({ - attemptsLeft: ok.body.attempts_left, - nextSend: AbsoluteTime.fromProtocolTimestamp( ok.body.next_tx_time), + changeTargetLeft: ok.body.attempts_left, + checkPinLeft: state?.lastStatus?.auth_attempts_left ?? 0, + sendCodeLeft: state?.lastStatus?.pin_transmissions_left ?? 0, + nextSend: AbsoluteTime.fromProtocolTimestamp( + ok.body.retransmission_time, + ), transmitted: ok.body.transmitted, }); } @@ -101,17 +107,15 @@ export function AskChallenge({ (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str``; - case HttpStatusCode.Forbidden: - return i18n.str``; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; } }, ); @@ -122,7 +126,7 @@ export function AskChallenge({ return ( <Fragment> - <LocalNotificationBanner notification={notification} /> + <LocalNotificationBanner notification={notification} showDebug={true} /> <div class="isolate bg-white px-6 py-12"> <div class="mx-auto max-w-2xl text-center"> @@ -213,16 +217,22 @@ export function AskChallenge({ </div> )} - {!status.changes_left ? ( - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate>No more changes left</i18n.Translate> - </p> - ) : ( - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate> - You can change your email address another{" "} - {status.changes_left} times. - </i18n.Translate> + {state.lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can't change the email anymore. + </i18n.Translate> + ) : state.lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the email one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the email {state.lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} </p> )} </div> diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts index e7b128b02..6a920749c 100644 --- a/packages/taler-util/src/http-client/challenger.ts +++ b/packages/taler-util/src/http-client/challenger.ts @@ -164,8 +164,6 @@ export class ChallengerHttpClient { } case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: @@ -205,7 +203,7 @@ export class ChallengerHttpClient { case HttpStatusCode.BadRequest: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); + return opKnownAlternativeFailure(resp, HttpStatusCode.Forbidden, codecForChallengeInvalidPinResponse()); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index a91e8cc71..3816b1598 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -1567,6 +1567,14 @@ export const codecForChallengerTermsOfServiceResponse = .property("name", codecForConstString("challenger")) .property("version", codecForString()) .property("implementation", codecOptional(codecForString())) + .property("restrictions", codecOptional(codecForMap(codecForAny()))) + .property( + "address_type", + codecForEither( + codecForConstString("phone"), + codecForConstString("email"), + ), + ) .build("ChallengerApi.ChallengerTermsOfServiceResponse"); export const codecForChallengeSetupResponse = @@ -1578,7 +1586,6 @@ export const codecForChallengeSetupResponse = export const codecForChallengeStatus = (): Codec<ChallengerApi.ChallengeStatus> => buildCodecForObject<ChallengerApi.ChallengeStatus>() - .property("restrictions", codecOptional(codecForMap(codecForAny()))) .property("fix_address", codecForBoolean()) .property("solved", codecForBoolean()) .property("last_address", codecOptional(codecForMap(codecForAny()))) @@ -1600,10 +1607,10 @@ export const codecForChallengeCreateResponse = (): Codec<ChallengerApi.ChallengeCreateResponse> => buildCodecForObject<ChallengerApi.ChallengeCreateResponse>() .property("attempts_left", codecForNumber()) - .property("address", codecForAny()) .property("type", codecForConstString("created")) + .property("address", codecForAny()) .property("transmitted", codecForBoolean()) - .property("next_tx_time", codecForTimestamp) + .property("retransmission_time", codecForTimestamp) .build("ChallengerApi.ChallengeCreateResponse"); export const codecForChallengeRedirect = @@ -5389,6 +5396,19 @@ export namespace ChallengerApi { // URN of the implementation (needed to interpret 'revision' in version). // @since v0, may become mandatory in the future. implementation?: string; + + // Object; map of keys (names of the fields of the address + // to be entered by the user) to objects with a "regex" (string) + // containing an extended Posix regular expression for allowed + // address field values, and a "hint"/"hint_i18n" giving a + // human-readable explanation to display if the value entered + // by the user does not match the regex. Keys that are not mapped + // to such an object have no restriction on the value provided by + // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. + restrictions: Record<string, Restriction> | undefined; + + // @since v2. + address_type: "email" | "phone"; } export interface ChallengeSetupResponse { @@ -5403,16 +5423,6 @@ export namespace ChallengerApi { } export interface ChallengeStatus { - // Object; map of keys (names of the fields of the address - // to be entered by the user) to objects with a "regex" (string) - // containing an extended Posix regular expression for allowed - // address field values, and a "hint"/"hint_i18n" giving a - // human-readable explanation to display if the value entered - // by the user does not match the regex. Keys that are not mapped - // to such an object have no restriction on the value provided by - // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration. - restrictions: Record<string, Restriction> | undefined; - // indicates if the given address cannot be changed anymore, the // form should be read-only if set to true. fix_address: boolean; @@ -5425,7 +5435,7 @@ export namespace ChallengerApi { // shown to the user changes_left: Integer; - // if the challenge has already been solved + // is the challenge already solved? solved: boolean; // when we would re-transmit the challenge the next @@ -5470,7 +5480,7 @@ export namespace ChallengerApi { // timestamp explaining when we would re-transmit the challenge the next // time (at the earliest) if requested by the user - next_tx_time: TalerProtocolTimestamp; + retransmission_time: TalerProtocolTimestamp; } export type ChallengeSolveResponse = ChallengeRedirect | InvalidPinResponse; |