anastasis: tag state properly

This commit is contained in:
Florian Dold 2022-04-13 08:44:37 +02:00
parent f3d8b44743
commit b28583ba7e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
15 changed files with 81 additions and 81 deletions

View File

@ -196,6 +196,7 @@ function getCountries(
export async function getBackupStartState(): Promise<ReducerStateBackup> { export async function getBackupStartState(): Promise<ReducerStateBackup> {
return { return {
reducer_type: "backup",
backup_state: BackupStates.ContinentSelecting, backup_state: BackupStates.ContinentSelecting,
continents: getContinents({ continents: getContinents({
requireProvider: true, requireProvider: true,
@ -205,6 +206,7 @@ export async function getBackupStartState(): Promise<ReducerStateBackup> {
export async function getRecoveryStartState(): Promise<ReducerStateRecovery> { export async function getRecoveryStartState(): Promise<ReducerStateRecovery> {
return { return {
reducer_type: "recovery",
recovery_state: RecoveryStates.ContinentSelecting, recovery_state: RecoveryStates.ContinentSelecting,
continents: getContinents({ continents: getContinents({
requireProvider: true, requireProvider: true,
@ -571,6 +573,7 @@ async function uploadSecret(
const talerPayUri = resp.headers.get("Taler"); const talerPayUri = resp.headers.get("Taler");
if (!talerPayUri) { if (!talerPayUri) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE, code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`, hint: `payment requested, but no taler://pay URI given`,
}; };
@ -579,6 +582,7 @@ async function uploadSecret(
const parsedUri = parsePayUri(talerPayUri); const parsedUri = parsePayUri(talerPayUri);
if (!parsedUri) { if (!parsedUri) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE, code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`, hint: `payment requested, but no taler://pay URI given`,
}; };
@ -587,6 +591,7 @@ async function uploadSecret(
continue; continue;
} }
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED, code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: `could not upload truth (HTTP status ${resp.status})`, hint: `could not upload truth (HTTP status ${resp.status})`,
}; };
@ -674,6 +679,7 @@ async function uploadSecret(
const talerPayUri = resp.headers.get("Taler"); const talerPayUri = resp.headers.get("Taler");
if (!talerPayUri) { if (!talerPayUri) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE, code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`, hint: `payment requested, but no taler://pay URI given`,
}; };
@ -682,6 +688,7 @@ async function uploadSecret(
const parsedUri = parsePayUri(talerPayUri); const parsedUri = parsePayUri(talerPayUri);
if (!parsedUri) { if (!parsedUri) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE, code: TalerErrorCode.ANASTASIS_REDUCER_BACKEND_FAILURE,
hint: `payment requested, but no taler://pay URI given`, hint: `payment requested, but no taler://pay URI given`,
}; };
@ -690,6 +697,7 @@ async function uploadSecret(
continue; continue;
} }
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED, code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
hint: `could not upload policy (http status ${resp.status})`, hint: `could not upload policy (http status ${resp.status})`,
}; };
@ -734,13 +742,13 @@ async function downloadPolicy(
throw Error("invalid state"); throw Error("invalid state");
} }
for (const prov of state.selected_version.providers) { for (const prov of state.selected_version.providers) {
const pi = state.authentication_providers?.[prov.provider_url]; const pi = state.authentication_providers?.[prov.url];
if (!pi || pi.status !== "ok") { if (!pi || pi.status !== "ok") {
continue; continue;
} }
const userId = await userIdentifierDerive(userAttributes, pi.salt); const userId = await userIdentifierDerive(userAttributes, pi.salt);
const acctKeypair = accountKeypairDerive(userId); const acctKeypair = accountKeypairDerive(userId);
const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.provider_url); const reqUrl = new URL(`policy/${acctKeypair.pub}`, prov.url);
reqUrl.searchParams.set("version", `${prov.version}`); reqUrl.searchParams.set("version", `${prov.version}`);
const resp = await fetch(reqUrl.href); const resp = await fetch(reqUrl.href);
if (resp.status !== 200) { if (resp.status !== 200) {
@ -759,7 +767,7 @@ async function downloadPolicy(
policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0"); policyVersion = Number(resp.headers.get("Anastasis-Version") ?? "0");
} catch (e) {} } catch (e) {}
foundRecoveryInfo = { foundRecoveryInfo = {
provider_url: prov.provider_url, provider_url: prov.url,
secret_name: rd.secret_name ?? "<unknown>", secret_name: rd.secret_name ?? "<unknown>",
version: policyVersion, version: policyVersion,
}; };
@ -768,6 +776,7 @@ async function downloadPolicy(
} }
if (!foundRecoveryInfo || !recoveryDoc) { if (!foundRecoveryInfo || !recoveryDoc) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED, code: TalerErrorCode.ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED,
hint: "No backups found at any provider for your identity information.", hint: "No backups found at any provider for your identity information.",
}; };
@ -874,7 +883,7 @@ async function pollChallenges(
const s2 = await requestTruth(state, truth, { const s2 = await requestTruth(state, truth, {
pin: feedback.answer_code, pin: feedback.answer_code,
}); });
if (s2.recovery_state) { if (s2.reducer_type === "recovery") {
state = s2; state = s2;
} }
} }
@ -1001,6 +1010,7 @@ async function requestTruth(
} }
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "got unexpected /truth/ response status", hint: "got unexpected /truth/ response status",
http_status: resp.status, http_status: resp.status,
@ -1110,6 +1120,7 @@ async function selectChallenge(
// FIXME: look at response, include in challenge_feedback! // FIXME: look at response, include in challenge_feedback!
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "got unexpected /truth/.../challenge response status", hint: "got unexpected /truth/.../challenge response status",
http_status: resp.status, http_status: resp.status,
@ -1125,6 +1136,7 @@ async function backupSelectContinent(
}); });
if (countries.length <= 0) { if (countries.length <= 0) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
hint: "continent not found", hint: "continent not found",
}; };
@ -1423,10 +1435,14 @@ async function nextFromChallengeSelecting(
args: void, args: void,
): Promise<ReducerStateRecovery | ReducerStateError> { ): Promise<ReducerStateRecovery | ReducerStateError> {
const s2 = await tryRecoverSecret(state); const s2 = await tryRecoverSecret(state);
if (s2.recovery_state === RecoveryStates.RecoveryFinished) { if (
s2.reducer_type === "recovery" &&
s2.recovery_state === RecoveryStates.RecoveryFinished
) {
return s2; return s2;
} }
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: "Not enough challenges solved", hint: "Not enough challenges solved",
}; };
@ -1462,7 +1478,7 @@ export function mergeDiscoveryAggregate(
const oldIndex = polHashToIndex[pol.policy_hash]; const oldIndex = polHashToIndex[pol.policy_hash];
if (oldIndex != null) { if (oldIndex != null) {
aggregatedPolicies[oldIndex].providers.push({ aggregatedPolicies[oldIndex].providers.push({
provider_url: pol.provider_url, url: pol.provider_url,
version: pol.version, version: pol.version,
}); });
} else { } else {
@ -1471,7 +1487,7 @@ export function mergeDiscoveryAggregate(
policy_hash: pol.policy_hash, policy_hash: pol.policy_hash,
providers: [ providers: [
{ {
provider_url: pol.provider_url, url: pol.provider_url,
version: pol.version, version: pol.version,
}, },
], ],
@ -1592,7 +1608,7 @@ const recoveryTransitions: Record<
...transition("add_provider", codecForAny(), addProviderRecovery), ...transition("add_provider", codecForAny(), addProviderRecovery),
...transition("delete_provider", codecForAny(), deleteProviderRecovery), ...transition("delete_provider", codecForAny(), deleteProviderRecovery),
...transition( ...transition(
"change_version", "select_version",
codecForActionArgsChangeVersion(), codecForActionArgsChangeVersion(),
changeVersion, changeVersion,
), ),
@ -1621,7 +1637,7 @@ export async function discoverPolicies(
state: ReducerState, state: ReducerState,
cursor?: DiscoveryCursor, cursor?: DiscoveryCursor,
): Promise<DiscoveryResult> { ): Promise<DiscoveryResult> {
if (!state.recovery_state) { if (state.reducer_type !== "recovery") {
throw Error("can only discover providers in recovery state"); throw Error("can only discover providers in recovery state");
} }
@ -1685,12 +1701,14 @@ export async function reduceAction(
h = recoveryTransitions[state.recovery_state][action]; h = recoveryTransitions[state.recovery_state][action];
} else { } else {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Invalid state (needs backup_state or recovery_state)`, hint: `Invalid state (needs backup_state or recovery_state)`,
}; };
} }
if (!h) { if (!h) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_ACTION_INVALID,
hint: `Unsupported action '${action}' in state '${stateName}'`, hint: `Unsupported action '${action}' in state '${stateName}'`,
}; };
@ -1700,9 +1718,10 @@ export async function reduceAction(
parsedArgs = h.argCodec.decode(args); parsedArgs = h.argCodec.decode(args);
} catch (e: any) { } catch (e: any) {
return { return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID, code: TalerErrorCode.ANASTASIS_REDUCER_INPUT_INVALID,
hint: "argument validation failed", hint: "argument validation failed",
message: e.toString(), detail: e.toString(),
}; };
} }
try { try {
@ -1710,7 +1729,10 @@ export async function reduceAction(
} catch (e) { } catch (e) {
logger.error("action handler failed"); logger.error("action handler failed");
if (e instanceof ReducerError) { if (e instanceof ReducerError) {
return e.errorJson; return {
reducer_type: "error",
...e.errorJson,
}
} }
throw e; throw e;
} }

View File

@ -73,19 +73,30 @@ export interface CoreSecret {
} }
export interface ReducerStateBackup { export interface ReducerStateBackup {
recovery_state?: undefined; reducer_type: "backup";
backup_state: BackupStates; backup_state: BackupStates;
code?: undefined;
currencies?: string[]; currencies?: string[];
continents?: ContinentInfo[]; continents?: ContinentInfo[];
countries?: CountryInfo[]; countries?: CountryInfo[];
identity_attributes?: { [n: string]: string }; identity_attributes?: { [n: string]: string };
authentication_providers?: { [url: string]: AuthenticationProviderStatus }; authentication_providers?: { [url: string]: AuthenticationProviderStatus };
authentication_methods?: AuthMethod[]; authentication_methods?: AuthMethod[];
required_attributes?: UserAttributeSpec[]; required_attributes?: UserAttributeSpec[];
selected_continent?: string; selected_continent?: string;
selected_country?: string; selected_country?: string;
secret_name?: string; secret_name?: string;
policies?: Policy[]; policies?: Policy[];
recovery_data?: { recovery_data?: {
@ -179,18 +190,10 @@ export interface RecoveryInformation {
} }
export interface ReducerStateRecovery { export interface ReducerStateRecovery {
reducer_type: "recovery";
recovery_state: RecoveryStates; recovery_state: RecoveryStates;
/**
* Unused in the recovery states.
*/
backup_state?: undefined;
/**
* Unused in the recovery states.
*/
code?: undefined;
identity_attributes?: { [n: string]: string }; identity_attributes?: { [n: string]: string };
continents?: ContinentInfo[]; continents?: ContinentInfo[];
@ -267,11 +270,10 @@ export interface TruthMetaData {
} }
export interface ReducerStateError { export interface ReducerStateError {
backup_state?: undefined; reducer_type: "error";
recovery_state?: undefined;
code: number; code: number;
hint?: string; hint?: string;
message?: string; detail?: string;
} }
export enum BackupStates { export enum BackupStates {
@ -302,12 +304,13 @@ export interface MethodSpec {
usage_fee: string; usage_fee: string;
} }
export type AuthenticationProviderStatusEmpty = { export type AuthenticationProviderStatusNotContacted = {
status: "not-contacted"; status: "not-contacted";
}; };
export interface AuthenticationProviderStatusOk { export interface AuthenticationProviderStatusOk {
status: "ok"; status: "ok";
annual_fee: string; annual_fee: string;
business_name: string; business_name: string;
currency: string; currency: string;
@ -320,8 +323,13 @@ export interface AuthenticationProviderStatusOk {
// FIXME: add timestamp? // FIXME: add timestamp?
} }
export interface AuthenticationProviderStatusDisabled {
status: "disabled";
}
export interface AuthenticationProviderStatusError { export interface AuthenticationProviderStatusError {
status: "error"; status: "error";
http_status?: number; http_status?: number;
code: number; code: number;
hint?: string; hint?: string;
@ -329,7 +337,8 @@ export interface AuthenticationProviderStatusError {
} }
export type AuthenticationProviderStatus = export type AuthenticationProviderStatus =
| AuthenticationProviderStatusEmpty | AuthenticationProviderStatusNotContacted
| AuthenticationProviderStatusDisabled
| AuthenticationProviderStatusError | AuthenticationProviderStatusError
| AuthenticationProviderStatusOk; | AuthenticationProviderStatusOk;
@ -486,7 +495,6 @@ export interface PolicyMetaInfo {
secret_name?: string; secret_name?: string;
} }
/** /**
* Aggregated / de-duplicated policy meta info. * Aggregated / de-duplicated policy meta info.
*/ */
@ -495,7 +503,7 @@ export interface AggregatedPolicyMetaInfo {
policy_hash: string; policy_hash: string;
attribute_mask: number; attribute_mask: number;
providers: { providers: {
provider_url: string; url: string;
version: number; version: number;
}[]; }[];
} }

View File

@ -19,7 +19,7 @@ export function AttributeEntryScreen(): VNode {
const [attrs, setAttrs] = useState<Record<string, string>>( const [attrs, setAttrs] = useState<Record<string, string>>(
currentIdentityAttributes, currentIdentityAttributes,
); );
const isBackup = state && state.backup_state; const isBackup = state?.reducer_type === "backup";
const [askUserIfSure, setAskUserIfSure] = useState(false); const [askUserIfSure, setAskUserIfSure] = useState(false);
if (!reducer) { if (!reducer) {

View File

@ -30,10 +30,7 @@ export function AuthenticationEditorScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const configuredAuthMethods: AuthMethod[] = const configuredAuthMethods: AuthMethod[] =
@ -62,7 +59,7 @@ export function AuthenticationEditorScreen(): VNode {
const authAvailableSet = new Set<string>(); const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) { for (const provKey of Object.keys(providers)) {
const p = providers[provKey]; const p = providers[provKey];
if ("http_status" in p && !("error_code" in p) && p.methods) { if (p.status === "ok") {
for (const meth of p.methods) { for (const meth of p.methods) {
authAvailableSet.add(meth.type); authAvailableSet.add(meth.type);
} }

View File

@ -9,10 +9,7 @@ export function BackupFinishedScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const details = reducer.currentReducerState.success_details; const details = reducer.currentReducerState.success_details;

View File

@ -55,10 +55,7 @@ export function ChallengeOverviewScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "recovery") {
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }

View File

@ -7,10 +7,7 @@ export function ChallengePayingScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "recovery") {
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const payments = [""]; //reducer.currentReducerState.payments ?? const payments = [""]; //reducer.currentReducerState.payments ??

View File

@ -38,10 +38,7 @@ export function EditPoliciesScreen({
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }

View File

@ -7,10 +7,7 @@ export function PoliciesPayingScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const payments = reducer.currentReducerState.policy_payment_requests ?? []; const payments = reducer.currentReducerState.policy_payment_requests ?? [];

View File

@ -18,10 +18,7 @@ export function RecoveryFinishedScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "recovery") {
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const secretName = reducer.currentReducerState.recovery_document?.secret_name; const secretName = reducer.currentReducerState.recovery_document?.secret_name;

View File

@ -15,10 +15,7 @@ export function ReviewPoliciesScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }

View File

@ -27,10 +27,7 @@ export function SecretEditorScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }

View File

@ -31,7 +31,7 @@ export function SecretSelectionScreen(): VNode {
if ( if (
!reducer.currentReducerState || !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined reducer.currentReducerState.reducer_type !== "recovery"
) { ) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
@ -73,14 +73,17 @@ export function SecretSelectionScreen(): VNode {
} }
return ( return (
<AnastasisClientFrame title="Recovery: Select secret" hideNext="Please select version to recover"> <AnastasisClientFrame
title="Recovery: Select secret"
hideNext="Please select version to recover"
>
<p>Found versions:</p> <p>Found versions:</p>
{policies.map((x) => ( {policies.map((x) => (
<div> <div>
{x.policy_hash} / {x.secret_name} {x.policy_hash} / {x.secret_name}
<button <button
onClick={async () => { onClick={async () => {
await reducer.transition("change_version", { await reducer.transition("select_version", {
selection: x, selection: x,
}); });
}} }}
@ -119,7 +122,7 @@ export function OldSecretSelectionScreen(): VNode {
} }
if ( if (
!reducer.currentReducerState || !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined reducer.currentReducerState.reducer_type !== "recovery"
) { ) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
@ -127,7 +130,7 @@ export function OldSecretSelectionScreen(): VNode {
async function doSelectVersion(p: string, n: number): Promise<void> { async function doSelectVersion(p: string, n: number): Promise<void> {
if (!reducer) return Promise.resolve(); if (!reducer) return Promise.resolve();
return reducer.runTransaction(async (tx) => { return reducer.runTransaction(async (tx) => {
await tx.transition("change_version", { await tx.transition("select_version", {
version: n, version: n,
provider_url: p, provider_url: p,
}); });

View File

@ -135,10 +135,7 @@ export function SolveScreen(): VNode {
</AnastasisClientFrame> </AnastasisClientFrame>
); );
} }
if ( if (reducer.currentReducerState?.reducer_type !== "recovery") {
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return ( return (
<AnastasisClientFrame hideNav title="Recovery problem"> <AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>

View File

@ -7,10 +7,7 @@ export function TruthsPayingScreen(): VNode {
if (!reducer) { if (!reducer) {
return <div>no reducer in context</div>; return <div>no reducer in context</div>;
} }
if ( if (reducer.currentReducerState?.reducer_type !== "backup") {
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>; return <div>invalid state</div>;
} }
const payments = reducer.currentReducerState.payments ?? []; const payments = reducer.currentReducerState.payments ?? [];