aboutsummaryrefslogtreecommitdiff
path: root/packages/challenger-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-06-30 17:09:46 -0300
committerSebastian <sebasjm@gmail.com>2024-06-30 23:46:47 -0300
commite0fa99e21e026e77f3143bf9e62573f9707f2e25 (patch)
tree26f28836b0ba8ec3e102b8cacc0cc1fe25885e49 /packages/challenger-ui/src
parent4133128c4fd795bb3fb34e2f49e43c2f53af72ef (diff)
downloadwallet-core-e0fa99e21e026e77f3143bf9e62573f9707f2e25.tar.xz
removed lastTry, added remember
Diffstat (limited to 'packages/challenger-ui/src')
-rw-r--r--packages/challenger-ui/src/components/CheckChallengeIsUpToDate.tsx6
-rw-r--r--packages/challenger-ui/src/hooks/session.ts29
-rw-r--r--packages/challenger-ui/src/pages/AnswerChallenge.tsx167
-rw-r--r--packages/challenger-ui/src/pages/AskChallenge.tsx243
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 &quot;{lastEmail}&quot;.
</i18n.Translate>
@@ -185,7 +169,18 @@ export function AnswerChallenge({
<i18n.Translate>
We recently already sent a TAN to your address &quot;
{lastEmail}&quot;. A new TAN will not be transmitted again
- before &quot;{state.lastTry.nextSend}&quot;.
+ before &quot;
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={
+ state.lastStatus?.retransmission_time === undefined
+ ? undefined
+ : AbsoluteTime.fromProtocolTimestamp(
+ state.lastStatus?.retransmission_time,
+ )
+ }
+ />
+ &quot;.
</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&#39;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&#39;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&#39;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&#39;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&#39;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&#39;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&#39;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&#39;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">