feedback state rendering

This commit is contained in:
Sebastian 2021-11-05 14:56:03 -03:00
parent d43ab6af87
commit a9d2a4654b
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
7 changed files with 428 additions and 77 deletions

View File

@ -46,10 +46,10 @@ function messageStyle(type: MessageType): string {
export function Notifications({ notifications, removeNotification }: Props): VNode { export function Notifications({ notifications, removeNotification }: Props): VNode {
return <div class="block"> 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"> <div class="message-header">
<p>{n.message}</p> <p>{n.message}</p>
<button class="delete" onClick={() => removeNotification && removeNotification(n)} /> {removeNotification && <button class="delete" onClick={() => removeNotification && removeNotification(n)} />}
</div> </div>
{n.description && <div class="message-body"> {n.description && <div class="message-body">
{n.description} {n.description}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
/* /*
This file is part of GNU Taler This file is part of GNU Taler
(C) 2021 Taler Systems S.A. (C) 2021 Taler Systems S.A.
@ -20,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { RecoveryStates, ReducerState } from "anastasis-core"; import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils"; import { createExample, reducerStatesExample } from "../../utils";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen"; import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
@ -176,16 +175,15 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
recovery_information: { recovery_information: {
policies: [ policies: [
[ [
{ uuid: "1" }, { uuid: "uuid-1" },
{ uuid: "2" }, { uuid: "uuid-2" },
{ uuid: "3" }, { uuid: "uuid-3" },
{ uuid: "4" }, { uuid: "uuid-4" },
{ uuid: "5" }, { uuid: "uuid-5" },
{ uuid: "6" }, { uuid: "uuid-6" },
{ uuid: "7" }, { uuid: "uuid-7" },
{ uuid: "8" }, { uuid: "uuid-8" },
{ uuid: "9" }, { uuid: "uuid-9" },
{ uuid: "10" },
], ],
], ],
challenges: [ challenges: [
@ -193,20 +191,96 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
cost: "USD:1", cost: "USD:1",
instructions: 'in state "solved"', instructions: 'in state "solved"',
type: "question", type: "question",
uuid: "1", uuid: "uuid-1",
}, },
{ {
cost: "USD:1", cost: "USD:1",
instructions: 'in state "message"', instructions: 'in state "message"',
type: "question", 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: { challenge_feedback: {
1: { state: "solved" }, "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
2: { state: "message", message: "Security question was not solved correctly" }, "uuid-2": {
// FIXME: add missing feedback states here! 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, } as ReducerState,
); );

View File

@ -12,27 +12,24 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message: case ChallengeFeedbackStatus.Message:
return ( return (
<div> <div class="block has-text-danger">{feedback.message}</div>
<p>{feedback.message}</p>
</div>
); );
case ChallengeFeedbackStatus.Solved:
return <div />
case ChallengeFeedbackStatus.Pending: case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
return null; return null;
case ChallengeFeedbackStatus.ServerFailure:
return <div class="block has-text-danger">Server error.</div>;
case ChallengeFeedbackStatus.RateLimitExceeded: case ChallengeFeedbackStatus.RateLimitExceeded:
return <div>Rate limit exceeded.</div>; return <div class="block has-text-danger">There were to many failed attempts.</div>;
case ChallengeFeedbackStatus.Redirect:
return <div>Redirect (FIXME: not supported)</div>;
case ChallengeFeedbackStatus.Unsupported: 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: 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: default:
return ( return <div />;
<div>
<pre>{JSON.stringify(feedback)}</pre>
</div>
);
} }
} }
@ -113,6 +110,77 @@ export function ChallengeOverviewScreen(): VNode {
const tableBody = policy.challenges.map(({ info, uuid }) => { const tableBody = policy.challenges.map(({ info, uuid }) => {
const isFree = !info.cost || info.cost.endsWith(":0"); const isFree = !info.cost || info.cost.endsWith(":0");
const method = authMethods[info.type as KnownAuthMethods]; 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 ( return (
<div <div
key={uuid} key={uuid}
@ -131,21 +199,9 @@ export function ChallengeOverviewScreen(): VNode {
</div> </div>
<OverviewFeedbackDisplay feedback={info.feedback} /> <OverviewFeedbackDisplay feedback={info.feedback} />
</div> </div>
<div>
{method && info.feedback?.state !== "solved" ? ( <ChallengeButton id={uuid} feedback={info.feedback} />
<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>
</div> </div>
); );
}); });
@ -156,8 +212,8 @@ export function ChallengeOverviewScreen(): VNode {
const opa = !atLeastThereIsOnePolicySolved const opa = !atLeastThereIsOnePolicySolved
? undefined ? undefined
: policy.isPolicySolved : policy.isPolicySolved
? undefined ? undefined
: "0.6"; : "0.6";
return ( return (
<div <div
key={policy_index} key={policy_index}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";

View File

@ -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 { 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 [otherProvider, setOtherProvider] = useState<string>(provider);
const [otherVersion, setOtherVersion] = useState(`${version}`); const [otherVersion, setOtherVersion] = useState(version > 0 ? String(version) : "");
return ( return (
<AnastasisClientFrame hideNav title="Recovery: Select secret"> <AnastasisClientFrame hideNav title="Recovery: Select secret">

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
/* /*
This file is part of GNU Taler This file is part of GNU Taler
(C) 2021 Taler Systems S.A. (C) 2021 Taler Systems S.A.
@ -20,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ReducerState } from 'anastasis-core'; import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils'; import { createExample, reducerStatesExample } from '../../utils';
import { SolveScreen as TestedComponent } from './SolveScreen'; import { SolveScreen as TestedComponent } from './SolveScreen';
@ -50,7 +49,8 @@ export const NotSupportedChallenge = createExample(TestedComponent, {
}], }],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1' selected_challenge_uuid: 'ASDASDSAD!1',
} as ReducerState); } as ReducerState);
export const MismatchedChallengeId = createExample(TestedComponent, { export const MismatchedChallengeId = createExample(TestedComponent, {
@ -78,7 +78,8 @@ export const SmsChallenge = createExample(TestedComponent, {
}], }],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1' selected_challenge_uuid: 'ASDASDSAD!1',
} as ReducerState); } as ReducerState);
export const QuestionChallenge = createExample(TestedComponent, { export const QuestionChallenge = createExample(TestedComponent, {
@ -92,7 +93,8 @@ export const QuestionChallenge = createExample(TestedComponent, {
}], }],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1' selected_challenge_uuid: 'ASDASDSAD!1',
} as ReducerState); } as ReducerState);
export const EmailChallenge = createExample(TestedComponent, { export const EmailChallenge = createExample(TestedComponent, {
@ -106,7 +108,8 @@ export const EmailChallenge = createExample(TestedComponent, {
}], }],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1' selected_challenge_uuid: 'ASDASDSAD!1',
} as ReducerState); } as ReducerState);
export const PostChallenge = createExample(TestedComponent, { export const PostChallenge = createExample(TestedComponent, {
@ -120,5 +123,181 @@ export const PostChallenge = createExample(TestedComponent, {
}], }],
policies: [], policies: [],
}, },
selected_challenge_uuid: 'ASDASDSAD!1' selected_challenge_uuid: 'ASDASDSAD!1',
} as ReducerState); } 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);

View File

@ -8,31 +8,67 @@ import {
} from "../../../../anastasis-core/lib"; } from "../../../../anastasis-core/lib";
import { AsyncButton } from "../../components/AsyncButton"; import { AsyncButton } from "../../components/AsyncButton";
import { TextInput } from "../../components/fields/TextInput"; import { TextInput } from "../../components/fields/TextInput";
import { Notifications } from "../../components/Notifications";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) { function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }): VNode {
const { feedback } = props; const { feedback } = props;
if (!feedback) { if (!feedback) {
return null; return <div />;
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message: case ChallengeFeedbackStatus.Message:
return ( return (<Notifications notifications={[{
<div> type: "INFO",
<p>{feedback.message}</p> message: `Message from provider`,
</div> description: feedback.message
); }]} />);
case ChallengeFeedbackStatus.Pending: 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: 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: 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: 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: 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: 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: default:
return ( return (
<div> <div>
@ -79,8 +115,8 @@ export function SolveScreen(): VNode {
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button> <button class="button" onClick={() => reducer.back()}>Back</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
@ -114,17 +150,23 @@ export function SolveScreen(): VNode {
reducer?.back(); 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 ( return (
<AnastasisClientFrame hideNav title="Recovery: Solve challenge"> <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
<SolveOverviewFeedbackDisplay <SolveOverviewFeedbackDisplay
feedback={challengeFeedback[selectedUuid]} feedback={feedback}
/> />
<SolveDialog <SolveDialog
id={selectedUuid} id={selectedUuid}
answer={answer} answer={answer}
setAnswer={setAnswer} setAnswer={setAnswer}
challenge={selectedChallenge} challenge={selectedChallenge}
feedback={challengeFeedback[selectedUuid]} feedback={feedback}
/> />
<div <div
@ -137,9 +179,9 @@ export function SolveScreen(): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
<AsyncButton class="button is-info" onClick={onNext}> {!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>}
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -160,6 +202,7 @@ function SolveSmsEntry({
}: SolveEntryProps): VNode { }: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p> <p>
An sms has been sent to "<b>{challenge.instructions}</b>". Type the code An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
below below