diff options
author | Sebastian <sebasjm@gmail.com> | 2024-06-30 17:09:46 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-06-30 23:46:47 -0300 |
commit | e0fa99e21e026e77f3143bf9e62573f9707f2e25 (patch) | |
tree | 26f28836b0ba8ec3e102b8cacc0cc1fe25885e49 | |
parent | 4133128c4fd795bb3fb34e2f49e43c2f53af72ef (diff) | |
download | wallet-core-e0fa99e21e026e77f3143bf9e62573f9707f2e25.tar.xz |
removed lastTry, added remember
4 files changed, 236 insertions, 209 deletions
diff --git a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx index ebfa57d02..5ac7998d8 100644 --- a/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx +++ b/packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx @@ -45,8 +45,8 @@ export function CheckChallengeIsUpToDate({ onNoMoreChanges, onNoInfo, }: Props): VNode { - const { state, updateStatus } = useSessionState(); - const {i18n} = useTranslationContext(); + const { state } = useSessionState(); + const { i18n } = useTranslationContext(); const sessionId = sessionFromParam ? sessionFromParam @@ -111,8 +111,6 @@ export function CheckChallengeIsUpToDate({ } } - updateStatus(result.body); - if (onCompleted && "redirectURL" in result.body) { onCompleted(); return <Loading />; diff --git a/packages/challenger-ui/src/hooks/session.ts b/packages/challenger-ui/src/hooks/session.ts index 4dc7e0dc1..03cef41bf 100644 --- a/packages/challenger-ui/src/hooks/session.ts +++ b/packages/challenger-ui/src/hooks/session.ts @@ -20,6 +20,7 @@ import { Codec, buildCodecForObject, codecForAbsoluteTime, + codecForAny, codecForBoolean, codecForChallengeStatus, codecForNumber, @@ -49,18 +50,10 @@ export type LastChallengeResponse = { }; export type SessionState = SessionId & { - lastTry: LastChallengeResponse | undefined; lastStatus: ChallengerApi.ChallengeStatus | undefined; completedURL: string | undefined; + lastAddress: Record<string, string> | undefined; }; -export const codecForLastChallengeResponse = (): Codec<LastChallengeResponse> => - buildCodecForObject<LastChallengeResponse>() - .property("sendCodeLeft", codecForNumber()) - .property("changeTargetLeft", codecForNumber()) - .property("checkPinLeft", codecForNumber()) - .property("nextSend", codecForAbsoluteTime) - .property("transmitted", codecForBoolean()) - .build("LastChallengeResponse"); export const codecForSessionState = (): Codec<SessionState> => buildCodecForObject<SessionState>() @@ -69,13 +62,15 @@ export const codecForSessionState = (): Codec<SessionState> => .property("state", codecForString()) .property("completedURL", codecOptional(codecForStringURL())) .property("lastStatus", codecOptional(codecForChallengeStatus())) - .property("lastTry", codecOptional(codecForLastChallengeResponse())) + .property("lastAddress", codecOptional(codecForAny())) .build("SessionState"); export interface SessionStateHandler { state: SessionState | undefined; start(s: SessionId): void; - accepted(l: LastChallengeResponse): void; + saveAddress(address: Record<string, string> | undefined): void; + sent(left: number, nextTime: AbsoluteTime): void; + failed(left: number): void; completed(e: URL): void; updateStatus(s: ChallengerApi.ChallengeStatus): void; } @@ -98,19 +93,15 @@ export function useSessionState(): SessionStateHandler { start(info) { update({ ...info, - lastTry: undefined, completedURL: undefined, lastStatus: undefined, + lastAddress: state?.lastAddress, }); cleanAllCache(); }, - accepted(lastTry) { - if (!state) return; - update({ - ...state, - lastTry, - }); - }, + saveAddress(address) {}, + sent(left: number, nextTime: AbsoluteTime) {}, + failed(left: number) {}, completed(url) { if (!state) return; update({ diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx index 2740e1bdb..ce2589ac5 100644 --- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -26,6 +26,7 @@ import { LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, + Time, useChallengerApiContext, useLocalNotificationHandler, useTranslationContext, @@ -51,7 +52,7 @@ export function AnswerChallenge({ }: Props): VNode { const { lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); - const { state, accepted, completed } = useSessionState(); + const { state, sent, failed, completed } = useSessionState(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [pin, setPin] = useState<string | undefined>(); @@ -67,29 +68,25 @@ export function AnswerChallenge({ ? undefined : state.lastStatus.last_address["email"]; + const contact = lastEmail ? { email: lastEmail } : undefined; + const onSendAgain = - lastEmail === undefined || + contact === undefined || state?.lastStatus == undefined || state?.lastStatus.changes_left === 0 ? undefined : withErrorHandler( async () => { - if (!lastEmail) return; - return await lib.challenger.challenge(nonce, { email: lastEmail }); + return await lib.challenger.challenge(nonce, contact); }, (ok) => { if (ok.body.type === "completed") { completed(new URL(ok.body.redirect_url)); } else { - accepted({ - 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, - }); + sent( + ok.body.attempts_left, + AbsoluteTime.fromProtocolTimestamp(ok.body.retransmission_time), + ); } return undefined; }, @@ -122,16 +119,7 @@ export function AnswerChallenge({ if (ok.body.type === "completed") { completed(new URL(ok.body.redirect_url)); } else { - 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, - }); + failed(ok.body.pin_transmissions_left); } onComplete(); }, @@ -160,10 +148,6 @@ export function AnswerChallenge({ return <div>no state</div>; } - if (!state.lastTry) { - return <div>you should do a challenge first</div>; - } - return ( <Fragment> <LocalNotificationBanner notification={notification} /> @@ -176,7 +160,7 @@ export function AnswerChallenge({ </i18n.Translate> </h2> <p class="mt-2 text-lg leading-8 text-gray-600"> - {state.lastTry.transmitted ? ( + {state.lastStatus?.last_address ? ( <i18n.Translate> A TAN was sent to your address "{lastEmail}". </i18n.Translate> @@ -185,7 +169,18 @@ export function AnswerChallenge({ <i18n.Translate> We recently already sent a TAN to your address " {lastEmail}". A new TAN will not be transmitted again - before "{state.lastTry.nextSend}". + before " + <Time + format="dd/MM/yyyy HH:mm:ss" + timestamp={ + state.lastStatus?.retransmission_time === undefined + ? undefined + : AbsoluteTime.fromProtocolTimestamp( + state.lastStatus?.retransmission_time, + ) + } + /> + ". </i18n.Translate> </Attention> )} @@ -201,7 +196,7 @@ export function AnswerChallenge({ </div> <form method="POST" - class="mx-auto mt-16 max-w-xl sm:mt-20" + class="mx-auto mt-4 max-w-xl sm:mt-20" onSubmit={(e) => { e.preventDefault(); }} @@ -236,24 +231,24 @@ export function AnswerChallenge({ </div> </div> - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate> - 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> + {state.lastStatus === undefined ? undefined : ( + <p class="mt-3 text-sm leading-6 text-gray-400"> + {state.lastStatus.auth_attempts_left < 1 ? ( + <i18n.Translate> + You can't check the PIN anymore. + </i18n.Translate> + ) : state.lastStatus.auth_attempts_left === 1 ? ( + <i18n.Translate> + You can check the PIN one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can check the PIN {state.lastStatus.auth_attempts_left}{" "} + more times. + </i18n.Translate> + )} + </p> + )} </div> <div class="mt-10"> @@ -269,31 +264,33 @@ export function AnswerChallenge({ <div class="mt-10 flex justify-between"> <div> <a - data-disabled={!state.lastStatus || state.lastStatus.changes_left < 1} + data-disabled={ + !state.lastStatus || state.lastStatus.changes_left < 1 + } href={routeAsk.url({ nonce })} 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> - {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> + {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" @@ -303,22 +300,24 @@ 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> + {state.lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {state.lastStatus.pin_transmissions_left < 1 ? ( + <i18n.Translate> + We can't send you the code anymore. + </i18n.Translate> + ) : state.lastStatus.pin_transmissions_left === 1 ? ( + <i18n.Translate> + We can send the code one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + We can send the code{" "} + {state.lastStatus.pin_transmissions_left} 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 dc60562b7..c194f6fd5 100644 --- a/packages/challenger-ui/src/pages/AskChallenge.tsx +++ b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -47,7 +47,7 @@ export function AskChallenge({ routeSolveChallenge, focus, }: Props): VNode { - const { state, accepted, completed } = useSessionState(); + const { state, sent, saveAddress, completed } = useSessionState(); const { lib, config } = useChallengerApiContext(); const status = state?.lastStatus; @@ -61,6 +61,7 @@ export function AskChallenge({ const [notification, withErrorHandler] = useLocalNotificationHandler(); const [email, setEmail] = useState<string | undefined>(); const [repeat, setRepeat] = useState<string | undefined>(); + const [remember, setRemember] = useState<boolean>(false); const regexTest = regexEmail && regexEmail.regex ? new RegExp(regexEmail.regex) : EMAIL_REGEX; @@ -81,44 +82,42 @@ export function AskChallenge({ ? i18n.str`emails doesn't match` : undefined, }); + const contact = email ? { email } : undefined; - const onSend = errors - ? undefined - : withErrorHandler( - async () => { - return lib.challenger.challenge(nonce, { email: email! }); - }, - (ok) => { - if (ok.body.type === "completed") { - completed(new URL(ok.body.redirect_url)); - } else { - accepted({ - 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, - }); - } - onSendSuccesful(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The request was not accepted, try reloading the app.`; - case HttpStatusCode.NotFound: - return i18n.str`Challenge not found.`; - case HttpStatusCode.NotAcceptable: - return i18n.str`Server templates are missing due to misconfiguration.`; - case HttpStatusCode.TooManyRequests: - return i18n.str`There have been too many attempts to request challenge transmissions.`; - case HttpStatusCode.InternalServerError: - return i18n.str`Server is not able to respond due to internal problems.`; - } - }, - ); + const onSend = + errors || !contact + ? undefined + : withErrorHandler( + async () => { + return lib.challenger.challenge(nonce, contact); + }, + (ok) => { + if (ok.body.type === "completed") { + completed(new URL(ok.body.redirect_url)); + } else { + saveAddress(contact); + sent( + ok.body.attempts_left, + AbsoluteTime.fromProtocolTimestamp(ok.body.retransmission_time), + ); + } + onSendSuccesful(); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The request was not accepted, try reloading the app.`; + case HttpStatusCode.NotFound: + return i18n.str`Challenge not found.`; + case HttpStatusCode.NotAcceptable: + return i18n.str`Server templates are missing due to misconfiguration.`; + case HttpStatusCode.TooManyRequests: + return i18n.str`There have been too many attempts to request challenge transmissions.`; + case HttpStatusCode.InternalServerError: + return i18n.str`Server is not able to respond due to internal problems.`; + } + }, + ); if (!status) { return <div>no status loaded</div>; @@ -140,7 +139,7 @@ export function AskChallenge({ </i18n.Translate> </p> </div> - {state.lastTry && ( + {state.lastStatus?.last_address && ( <Fragment> <Attention title={i18n.str`A code has been sent to ${prevEmail}`}> <i18n.Translate> @@ -153,89 +152,129 @@ export function AskChallenge({ )} <form method="POST" - class="mx-auto mt-16 max-w-xl sm:mt-20" + class="mx-auto mt-4 max-w-xl sm:mt-20" onSubmit={(e) => { e.preventDefault(); }} > - <div class="grid grid-cols-1 gap-x-8 gap-y-6"> + <div class="py-4"> + <Attention title={i18n.str`A code has been sent to ${prevEmail}`}> + <i18n.Translate> + <a href={routeSolveChallenge.url({ nonce })} class="underline"> + <i18n.Translate>Complete the challenge here.</i18n.Translate> + </a> + </i18n.Translate> + </Attention> + </div> + + <div class="sm:col-span-2"> + <label + for="email" + class="block text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Email</i18n.Translate> + </label> + <div class="mt-2.5"> + <input + type="email" + name="email" + id="email" + ref={focus ? doAutoFocus : undefined} + maxLength={512} + autocomplete="email" + value={email} + onChange={(e) => { + setEmail(e.currentTarget.value); + }} + placeholder={prevEmail} + readOnly={status.fix_address} + class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + /> + <ShowInputErrorLabel + message={errors?.email} + isDirty={email !== undefined} + /> + </div> + </div> + + <div class="flex items-center justify-between py-2"> + <span class="flex flex-grow flex-col"> + <span + class="text-sm text-black font-medium leading-6 " + id="availability-label" + > + <i18n.Translate> + Remember this address for future challenges. + </i18n.Translate> + </span> + </span> + <button + type="button" + name={`remember switch`} + data-enabled={remember} + class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" + role="switch" + aria-checked="false" + aria-labelledby="availability-label" + aria-describedby="availability-description" + onClick={() => { + setRemember(!remember); + }} + > + <span + aria-hidden="true" + data-enabled={remember} + class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + ></span> + </button> + </div> + + {status.fix_address ? undefined : ( <div class="sm:col-span-2"> <label - for="email" + for="repeat-email" class="block text-sm font-semibold leading-6 text-gray-900" > - <i18n.Translate>Email</i18n.Translate> + <i18n.Translate>Repeat email</i18n.Translate> </label> <div class="mt-2.5"> <input type="email" - name="email" - id="email" - ref={focus ? doAutoFocus : undefined} - maxLength={512} - autocomplete="email" - value={email} + name="repeat-email" + id="repeat-email" + value={repeat} onChange={(e) => { - setEmail(e.currentTarget.value); + setRepeat(e.currentTarget.value); }} - placeholder={prevEmail} - readOnly={status.fix_address} - class="block w-full read-only:bg-slate-200 rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + autocomplete="email" + class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" /> <ShowInputErrorLabel - message={errors?.email} - isDirty={email !== undefined} + message={errors?.repeat} + isDirty={repeat !== undefined} /> </div> </div> + )} - {status.fix_address ? undefined : ( - <div class="sm:col-span-2"> - <label - for="repeat-email" - class="block text-sm font-semibold leading-6 text-gray-900" - > - <i18n.Translate>Repeat email</i18n.Translate> - </label> - <div class="mt-2.5"> - <input - type="email" - name="repeat-email" - id="repeat-email" - value={repeat} - onChange={(e) => { - setRepeat(e.currentTarget.value); - }} - autocomplete="email" - class="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - /> - <ShowInputErrorLabel - message={errors?.repeat} - isDirty={repeat !== undefined} - /> - </div> - </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> + {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> + )} {!prevEmail ? ( <div class="mt-10"> |