diff options
author | Sebastian <sebasjm@gmail.com> | 2021-11-05 14:56:03 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-11-05 14:56:28 -0300 |
commit | a9d2a4654b414a01a53a0d79f1f90a5102564710 (patch) | |
tree | a5dc5c76ea7ffeaec101169699302a40e0c63131 /packages/anastasis-webui | |
parent | d43ab6af87f2729b37548336d42ffce0ed3c879e (diff) |
feedback state rendering
Diffstat (limited to 'packages/anastasis-webui')
7 files changed, 428 insertions, 77 deletions
diff --git a/packages/anastasis-webui/src/components/Notifications.tsx b/packages/anastasis-webui/src/components/Notifications.tsx index c916020d7..097ebb4de 100644 --- a/packages/anastasis-webui/src/components/Notifications.tsx +++ b/packages/anastasis-webui/src/components/Notifications.tsx @@ -46,10 +46,10 @@ function messageStyle(type: MessageType): string { export function Notifications({ notifications, removeNotification }: Props): VNode { return <div class="block"> - {notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}> + {notifications.map((n, i) => <article key={i} class={messageStyle(n.type)}> <div class="message-header"> <p>{n.message}</p> - <button class="delete" onClick={() => removeNotification && removeNotification(n)} /> + {removeNotification && <button class="delete" onClick={() => removeNotification && removeNotification(n)} />} </div> {n.description && <div class="message-body"> {n.description} diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx index 48115c798..e001ed157 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.stories.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ /* This file is part of GNU Taler (C) 2021 Taler Systems S.A. @@ -20,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { RecoveryStates, ReducerState } from "anastasis-core"; +import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from "anastasis-core"; import { createExample, reducerStatesExample } from "../../utils"; import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen"; @@ -176,16 +175,15 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample( recovery_information: { policies: [ [ - { uuid: "1" }, - { uuid: "2" }, - { uuid: "3" }, - { uuid: "4" }, - { uuid: "5" }, - { uuid: "6" }, - { uuid: "7" }, - { uuid: "8" }, - { uuid: "9" }, - { uuid: "10" }, + { uuid: "uuid-1" }, + { uuid: "uuid-2" }, + { uuid: "uuid-3" }, + { uuid: "uuid-4" }, + { uuid: "uuid-5" }, + { uuid: "uuid-6" }, + { uuid: "uuid-7" }, + { uuid: "uuid-8" }, + { uuid: "uuid-9" }, ], ], challenges: [ @@ -193,20 +191,96 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample( cost: "USD:1", instructions: 'in state "solved"', type: "question", - uuid: "1", + uuid: "uuid-1", }, { cost: "USD:1", instructions: 'in state "message"', type: "question", - uuid: "2", + uuid: "uuid-2", + }, + { + cost: "USD:1", + instructions: 'in state "auth iban"', + type: "question", + uuid: "uuid-3", + }, + { + cost: "USD:1", + instructions: 'in state "payment "', + type: "question", + uuid: "uuid-4", + }, + { + cost: "USD:1", + instructions: 'in state "rate limit"', + type: "question", + uuid: "uuid-5", + }, + { + cost: "USD:1", + instructions: 'in state "redirect"', + type: "question", + uuid: "uuid-6", + }, + { + cost: "USD:1", + instructions: 'in state "server failure"', + type: "question", + uuid: "uuid-7", + }, + { + cost: "USD:1", + instructions: 'in state "truth unknown"', + type: "question", + uuid: "uuid-8", + }, + { + cost: "USD:1", + instructions: 'in state "unsupported"', + type: "question", + uuid: "uuid-9", }, ], }, challenge_feedback: { - 1: { state: "solved" }, - 2: { state: "message", message: "Security question was not solved correctly" }, - // FIXME: add missing feedback states here! + "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() }, + "uuid-2": { + state: ChallengeFeedbackStatus.Message.toString(), + message: 'Challenge should be solved' + }, + "uuid-3": { + state: ChallengeFeedbackStatus.AuthIban.toString(), + challenge_amount: "EUR:1", + credit_iban: "DE12345789000", + business_name: "Data Loss Incorporated", + wire_transfer_subject: "Anastasis 987654321" + }, + "uuid-4": { + state: ChallengeFeedbackStatus.Payment.toString(), + taler_pay_uri: "taler://pay/...", + provider: "https://localhost:8080/", + payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG" + }, + "uuid-5": { + state: ChallengeFeedbackStatus.RateLimitExceeded.toString(), + // "error_code": 8121 + }, + "uuid-6": { + state: ChallengeFeedbackStatus.Redirect.toString(), + redirect_url: "https://videoconf.example.com/", + http_status: 303 + }, + "uuid-7": { + state: ChallengeFeedbackStatus.ServerFailure.toString(), + http_status: 500, + error_response: "some error message or error object", + }, + "uuid-8": { + state: ChallengeFeedbackStatus.TruthUnknown.toString(), + // "error_code": 8108 + }, + "uuid-9": { state: ChallengeFeedbackStatus.Unsupported.toString() }, }, } as ReducerState, ); diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx index ed34bbde2..598999f2e 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx @@ -12,27 +12,24 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) { switch (feedback.state) { case ChallengeFeedbackStatus.Message: return ( - <div> - <p>{feedback.message}</p> - </div> + <div class="block has-text-danger">{feedback.message}</div> ); + case ChallengeFeedbackStatus.Solved: + return <div /> case ChallengeFeedbackStatus.Pending: case ChallengeFeedbackStatus.AuthIban: return null; + case ChallengeFeedbackStatus.ServerFailure: + return <div class="block has-text-danger">Server error.</div>; case ChallengeFeedbackStatus.RateLimitExceeded: - return <div>Rate limit exceeded.</div>; - case ChallengeFeedbackStatus.Redirect: - return <div>Redirect (FIXME: not supported)</div>; + return <div class="block has-text-danger">There were to many failed attempts.</div>; case ChallengeFeedbackStatus.Unsupported: - return <div>Challenge not supported by client.</div>; + return <div class="block has-text-danger">This client doesn't support solving this type of challenge. Use another version or contact the provider.</div>; case ChallengeFeedbackStatus.TruthUnknown: - return <div>Truth unknown</div>; + return <div class="block has-text-danger">Provider doesn't recognize the challenge of the policy. Contact the provider for further information.</div>; + case ChallengeFeedbackStatus.Redirect: default: - return ( - <div> - <pre>{JSON.stringify(feedback)}</pre> - </div> - ); + return <div />; } } @@ -113,6 +110,77 @@ export function ChallengeOverviewScreen(): VNode { const tableBody = policy.challenges.map(({ info, uuid }) => { const isFree = !info.cost || info.cost.endsWith(":0"); const method = authMethods[info.type as KnownAuthMethods]; + + if (!method) { + return <div + key={uuid} + class="block" + style={{ display: "flex", justifyContent: "space-between" }} + > + <div style={{ display: "flex", alignItems: "center" }}> + <span>unknown challenge</span> + </div> + + </div> + } + + function ChallengeButton({ id, feedback }: { id: string; feedback?: ChallengeFeedback }): VNode { + function selectChallenge(): void { + if (reducer) reducer.transition("select_challenge", { uuid: id }) + } + if (!feedback) { + return <div> + <button class="button" onClick={selectChallenge}> + Solve + </button> + </div> + } + switch (feedback.state) { + case ChallengeFeedbackStatus.ServerFailure: + case ChallengeFeedbackStatus.Unsupported: + case ChallengeFeedbackStatus.TruthUnknown: + case ChallengeFeedbackStatus.RateLimitExceeded: return <div /> + case ChallengeFeedbackStatus.AuthIban: + case ChallengeFeedbackStatus.Payment: return <div> + <button class="button" onClick={selectChallenge}> + Pay + </button> + </div> + case ChallengeFeedbackStatus.Redirect: return <div> + <button class="button" onClick={selectChallenge}> + Go to {feedback.redirect_url} + </button> + </div> + case ChallengeFeedbackStatus.Solved: return <div> + <div class="tag is-success is-large"> + Solved + </div> + </div> + default: return <div> + <button class="button" onClick={selectChallenge}> + Solve + </button> + </div> + + } + // return <div> + // {feedback.state !== "solved" ? ( + // <a + // class="button" + // onClick={() => + + // } + // > + // {isFree ? "Solve" : `Pay and Solve`} + // </a> + // ) : null} + // {feedback.state === "solved" ? ( + // // <div class="block is-success" > Solved </div> + // <div class="tag is-success is-large">Solved</div> + + // ) : null} + // </div> + } return ( <div key={uuid} @@ -131,21 +199,9 @@ export function ChallengeOverviewScreen(): VNode { </div> <OverviewFeedbackDisplay feedback={info.feedback} /> </div> - <div> - {method && info.feedback?.state !== "solved" ? ( - <a - class="button" - onClick={() => - reducer.transition("select_challenge", { uuid }) - } - > - {isFree ? "Solve" : `Pay and Solve`} - </a> - ) : null} - {info.feedback?.state === "solved" ? ( - <a class="button is-success"> Solved </a> - ) : null} - </div> + + <ChallengeButton id={uuid} feedback={info.feedback} /> + </div> ); }); @@ -156,8 +212,8 @@ export function ChallengeOverviewScreen(): VNode { const opa = !atLeastThereIsOnePolicySolved ? undefined : policy.isPolicySolved - ? undefined - : "0.6"; + ? undefined + : "0.6"; return ( <div key={policy_index} diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx index f93963f67..aa98b5dd9 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx index 8aa5ed2f7..cf38d3f18 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx @@ -104,7 +104,7 @@ function ChooseAnotherProviderScreen({ providers, selected, onChange }: { select function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode { const [otherProvider, setOtherProvider] = useState<string>(provider); - const [otherVersion, setOtherVersion] = useState(`${version}`); + const [otherVersion, setOtherVersion] = useState(version > 0 ? String(version) : ""); return ( <AnastasisClientFrame hideNav title="Recovery: Select secret"> diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx index cb6561b3f..05c4bfa42 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ /* This file is part of GNU Taler (C) 2021 Taler Systems S.A. @@ -20,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ReducerState } from 'anastasis-core'; +import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core'; import { createExample, reducerStatesExample } from '../../utils'; import { SolveScreen as TestedComponent } from './SolveScreen'; @@ -50,7 +49,8 @@ export const NotSupportedChallenge = createExample(TestedComponent, { }], policies: [], }, - selected_challenge_uuid: 'ASDASDSAD!1' + selected_challenge_uuid: 'ASDASDSAD!1', + } as ReducerState); export const MismatchedChallengeId = createExample(TestedComponent, { @@ -78,7 +78,8 @@ export const SmsChallenge = createExample(TestedComponent, { }], policies: [], }, - selected_challenge_uuid: 'ASDASDSAD!1' + selected_challenge_uuid: 'ASDASDSAD!1', + } as ReducerState); export const QuestionChallenge = createExample(TestedComponent, { @@ -92,7 +93,8 @@ export const QuestionChallenge = createExample(TestedComponent, { }], policies: [], }, - selected_challenge_uuid: 'ASDASDSAD!1' + selected_challenge_uuid: 'ASDASDSAD!1', + } as ReducerState); export const EmailChallenge = createExample(TestedComponent, { @@ -106,7 +108,8 @@ export const EmailChallenge = createExample(TestedComponent, { }], policies: [], }, - selected_challenge_uuid: 'ASDASDSAD!1' + selected_challenge_uuid: 'ASDASDSAD!1', + } as ReducerState); export const PostChallenge = createExample(TestedComponent, { @@ -120,5 +123,181 @@ export const PostChallenge = createExample(TestedComponent, { }], policies: [], }, - selected_challenge_uuid: 'ASDASDSAD!1' + selected_challenge_uuid: 'ASDASDSAD!1', + +} as ReducerState); + + +export const QuestionChallengeMessageFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.Message, + message: 'Challenge should be solved' + } + } + +} as ReducerState); + +export const QuestionChallengeServerFailureFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.ServerFailure, + http_status: 500, + error_response: "Couldn't connect to mysql" + } + } + +} as ReducerState); + +export const QuestionChallengeRedirectFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.Redirect, + http_status: 302, + redirect_url: 'http://video.taler.net' + } + } + +} as ReducerState); + +export const QuestionChallengeMessageRateLimitExceededFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.RateLimitExceeded, + } + } + +} as ReducerState); + +export const QuestionChallengeUnsupportedFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.Unsupported, + http_status: 500, + unsupported_method: 'Question' + } + } + } as ReducerState); + +export const QuestionChallengeTruthUnknownFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.TruthUnknown, + } + } + +} as ReducerState); + +export const QuestionChallengeAuthIbanFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.AuthIban, + challenge_amount: "EUR:1", + credit_iban: "DE12345789000", + business_name: "Data Loss Incorporated", + wire_transfer_subject: "Anastasis 987654321" + } + } + +} as ReducerState); + +export const QuestionChallengePaymentFeedback = createExample(TestedComponent, { + ...reducerStatesExample.challengeSolving, + recovery_information: { + challenges: [{ + cost: 'USD:1', + instructions: 'does P equals NP?', + type: 'question', + uuid: 'ASDASDSAD!1' + }], + policies: [], + }, + selected_challenge_uuid: 'ASDASDSAD!1', + challenge_feedback: { + 'ASDASDSAD!1': { + state: ChallengeFeedbackStatus.Payment, + taler_pay_uri : "taler://pay/...", + provider : "https://localhost:8080/", + payment_secret : "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG" + } + } +} as ReducerState); + diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx index bc1a88db3..35db5ead0 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx @@ -8,31 +8,67 @@ import { } from "../../../../anastasis-core/lib"; import { AsyncButton } from "../../components/AsyncButton"; import { TextInput } from "../../components/fields/TextInput"; +import { Notifications } from "../../components/Notifications"; import { useAnastasisContext } from "../../context/anastasis"; -function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) { +function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }): VNode { const { feedback } = props; if (!feedback) { - return null; + return <div />; } switch (feedback.state) { case ChallengeFeedbackStatus.Message: - return ( - <div> - <p>{feedback.message}</p> - </div> - ); - case ChallengeFeedbackStatus.Pending: + return (<Notifications notifications={[{ + type: "INFO", + message: `Message from provider`, + description: feedback.message + }]} />); + case ChallengeFeedbackStatus.Payment: + return <Notifications notifications={[{ + type: "INFO", + message: `Message from provider`, + description: <span> + To pay you can <a href={feedback.taler_pay_uri}>click here</a> + </span> + }]} /> case ChallengeFeedbackStatus.AuthIban: - return null; + return <Notifications notifications={[{ + type: "INFO", + message: `Message from provider`, + description: `Need to send a wire transfer to "${feedback.business_name}"` + }]} />; + case ChallengeFeedbackStatus.ServerFailure: + return (<Notifications notifications={[{ + type: "ERROR", + message: `Server error: Code ${feedback.http_status}`, + description: feedback.error_response + }]} />); case ChallengeFeedbackStatus.RateLimitExceeded: - return <div>Rate limit exceeded.</div>; + return (<Notifications notifications={[{ + type: "ERROR", + message: `Message from provider`, + description: "There were to many failed attempts." + }]} />); case ChallengeFeedbackStatus.Redirect: - return <div>Redirect (FIXME: not supported)</div>; + return (<Notifications notifications={[{ + type: "INFO", + message: `Message from provider`, + description: <span> + Please visit this link: <a>{feedback.redirect_url}</a> + </span> + }]} />); case ChallengeFeedbackStatus.Unsupported: - return <div>Challenge not supported by client.</div>; + return (<Notifications notifications={[{ + type: "ERROR", + message: `This client doesn't support solving this type of challenge`, + description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"` + }]} />); case ChallengeFeedbackStatus.TruthUnknown: - return <div>Truth unknown</div>; + return (<Notifications notifications={[{ + type: "ERROR", + message: `Provider doesn't recognize the type of challenge`, + description: "Contact the provider for further information" + }]} />); default: return ( <div> @@ -79,8 +115,8 @@ export function SolveScreen(): VNode { <AnastasisClientFrame hideNav title="Recovery problem"> <div>invalid state</div> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> - <button class="button" onClick={() => reducer.back()}>Back</button> - </div> + <button class="button" onClick={() => reducer.back()}>Back</button> + </div> </AnastasisClientFrame> ); } @@ -114,17 +150,23 @@ export function SolveScreen(): VNode { reducer?.back(); } + const feedback = challengeFeedback[selectedUuid] + const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded + || feedback?.state === ChallengeFeedbackStatus.Redirect + || feedback?.state === ChallengeFeedbackStatus.Unsupported + || feedback?.state === ChallengeFeedbackStatus.TruthUnknown + return ( <AnastasisClientFrame hideNav title="Recovery: Solve challenge"> <SolveOverviewFeedbackDisplay - feedback={challengeFeedback[selectedUuid]} + feedback={feedback} /> <SolveDialog id={selectedUuid} answer={answer} setAnswer={setAnswer} challenge={selectedChallenge} - feedback={challengeFeedback[selectedUuid]} + feedback={feedback} /> <div @@ -137,9 +179,9 @@ export function SolveScreen(): VNode { <button class="button" onClick={onCancel}> Cancel </button> - <AsyncButton class="button is-info" onClick={onNext}> + {!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}> Confirm - </AsyncButton> + </AsyncButton>} </div> </AnastasisClientFrame> ); @@ -160,6 +202,7 @@ function SolveSmsEntry({ }: SolveEntryProps): VNode { return ( <Fragment> + <p> An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below |