Skip to content

Commit 1c72f05

Browse files
committed
portal: Fix fraud protection cannot be turned off #5728
2 parents fe1fb93 + 077355f commit 1c72f05

3 files changed

Lines changed: 126 additions & 26 deletions

File tree

portal/src/hook/useAppConfigForm.ts

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export interface AppConfigFormModel<State> {
1919
reload: () => void;
2020
reset: () => void;
2121
save: (ignoreConflict?: boolean) => Promise<void>;
22+
saveWith: (
23+
fn: (state: State) => State,
24+
ignoreConflict?: boolean
25+
) => Promise<void>;
2226
setCanSave: (canSave?: boolean) => void;
2327
effectiveConfig: PortalAPIAppConfig;
2428
}
@@ -104,25 +108,18 @@ export function useAppConfigForm<State>(
104108
setIsSubmitted(false);
105109
}, [isUpdating]);
106110

107-
const save = useCallback(
108-
async (ignoreConflict: boolean = false) => {
109-
const allowSave = canSave !== undefined ? canSave : isDirty;
110-
if (!rawConfig || !initialState || secretConfig == null) {
111-
return;
112-
} else if (!allowSave || isUpdating) {
113-
return;
114-
}
115-
116-
const err = validate?.(currentState ?? initialState);
111+
const performSave = useCallback(
112+
async (stateToSave: State, ignoreConflict: boolean) => {
113+
const err = validate?.(stateToSave);
117114
if (err) {
118115
setUpdateError(err);
119116
return;
120117
}
121118

122119
const newConfig = constructConfig(
123-
rawConfig,
120+
rawConfig!,
124121
initialState,
125-
currentState ?? initialState,
122+
stateToSave,
126123
effectiveConfig
127124
);
128125

@@ -132,7 +129,7 @@ export function useAppConfigForm<State>(
132129
await updateConfig({
133130
appConfig: newConfig,
134131
appConfigChecksum: rawAppConfigChecksum,
135-
ignoreConflict: ignoreConflict,
132+
ignoreConflict,
136133
});
137134
await reload();
138135
setCurrentState(null);
@@ -144,20 +141,56 @@ export function useAppConfigForm<State>(
144141
setIsUpdating(false);
145142
}
146143
},
144+
[
145+
validate,
146+
constructConfig,
147+
rawConfig,
148+
initialState,
149+
effectiveConfig,
150+
updateConfig,
151+
rawAppConfigChecksum,
152+
reload,
153+
]
154+
);
155+
156+
const save = useCallback(
157+
async (ignoreConflict: boolean = false) => {
158+
const allowSave = canSave !== undefined ? canSave : isDirty;
159+
if (!rawConfig || !initialState || secretConfig == null) {
160+
return;
161+
} else if (!allowSave || isUpdating) {
162+
return;
163+
}
164+
await performSave(currentState ?? initialState, ignoreConflict);
165+
},
147166
[
148167
canSave,
149168
isDirty,
150169
rawConfig,
151170
initialState,
152171
secretConfig,
153172
isUpdating,
154-
validate,
155173
currentState,
156-
constructConfig,
157-
effectiveConfig,
158-
updateConfig,
159-
rawAppConfigChecksum,
160-
reload,
174+
performSave,
175+
]
176+
);
177+
178+
const saveWith = useCallback(
179+
async (fn: (state: State) => State, ignoreConflict: boolean = false) => {
180+
if (!rawConfig || !initialState || secretConfig == null || isUpdating) {
181+
return;
182+
}
183+
const newState = fn(currentState ?? initialState);
184+
setCurrentState(newState);
185+
await performSave(newState, ignoreConflict);
186+
},
187+
[
188+
rawConfig,
189+
initialState,
190+
secretConfig,
191+
isUpdating,
192+
currentState,
193+
performSave,
161194
]
162195
);
163196

@@ -185,6 +218,8 @@ export function useAppConfigForm<State>(
185218
reload,
186219
reset,
187220
save,
221+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
222+
saveWith,
188223
effectiveConfig,
189224
};
190225
}

portal/src/locale-data/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,6 +1900,10 @@
19001900
"FraudProtectionConfigurationScreen.allowlist.ip.country.description": "Requests from these countries (by IP geolocation) will not be blocked by fraud protection.",
19011901
"FraudProtectionConfigurationScreen.allowlist.phone.country.label": "Allow the following countries based on phone country code",
19021902
"FraudProtectionConfigurationScreen.allowlist.phone.country.description": "Requests from phone numbers registered in these countries will not be blocked by fraud protection.",
1903+
"FraudProtectionConfigurationScreen.disable.confirmation.title": "Disable Fraud Protection?",
1904+
"FraudProtectionConfigurationScreen.disable.confirmation.description": "Are you sure you want to disable Fraud Protection? This will stop all fraud detection and protection for your application.",
1905+
"FraudProtectionConfigurationScreen.disable.confirmation.confirm": "Disable",
1906+
"FraudProtectionConfigurationScreen.disable.confirmation.cancel": "Cancel",
19031907
"IntegrationsConfigurationScreen.title": "Integrations",
19041908
"IntegrationsConfigurationScreen.add-on": "Add-on",
19051909
"IntegrationsConfigurationScreen.action": "Action",

portal/src/screens/fraud-protection/FraudProtectionConfigurationScreen.tsx

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import React, { useCallback, useContext } from "react";
1+
import React, { useCallback, useContext, useState } from "react";
22
import { useParams, Navigate } from "react-router-dom";
3-
import { IChoiceGroupOption, ITag, PivotItem } from "@fluentui/react";
3+
import {
4+
Dialog,
5+
DialogFooter,
6+
IChoiceGroupOption,
7+
ITag,
8+
PivotItem,
9+
} from "@fluentui/react";
10+
import PrimaryButton from "../../PrimaryButton";
11+
import DefaultButton from "../../DefaultButton";
412
import { Address4, Address6 } from "ip-address";
513
import { produce } from "immer";
614
import { default as parseLibPhoneNumber } from "libphonenumber-js";
@@ -206,6 +214,7 @@ interface FraudProtectionConfigurationContentProps {
206214
selectedKey: FraudProtectionTab;
207215
onLinkClick: (item?: PivotItem) => void;
208216
onChangeKey: (key: FraudProtectionTab) => void;
217+
onToggleEnabledAndSave: (enabled: boolean) => void;
209218
}
210219

211220
const FraudProtectionConfigurationContent: React.VFC<FraudProtectionConfigurationContentProps> =
@@ -216,6 +225,7 @@ const FraudProtectionConfigurationContent: React.VFC<FraudProtectionConfiguratio
216225
selectedKey,
217226
onLinkClick,
218227
onChangeKey,
228+
onToggleEnabledAndSave,
219229
} = props;
220230
const { renderToString } = useContext(Context);
221231
const { state, setState } = form;
@@ -226,12 +236,9 @@ const FraudProtectionConfigurationContent: React.VFC<FraudProtectionConfiguratio
226236
_event: React.FormEvent<HTMLElement | HTMLInputElement>,
227237
checked?: boolean
228238
) => {
229-
setState((current) => ({
230-
...current,
231-
enabled: checked ?? false,
232-
}));
239+
onToggleEnabledAndSave(checked ?? false);
233240
},
234-
[setState]
241+
[onToggleEnabledAndSave]
235242
);
236243

237244
const onEnforcementModeChange = useCallback(
@@ -384,6 +391,9 @@ const FraudProtectionConfigurationContent: React.VFC<FraudProtectionConfiguratio
384391
const FraudProtectionConfigurationScreen: React.VFC =
385392
function FraudProtectionConfigurationScreen() {
386393
const { appID } = useParams() as { appID: string };
394+
const { renderToString } = useContext(Context);
395+
const [showDisableConfirmation, setShowDisableConfirmation] =
396+
useState(false);
387397
const form = useAppConfigForm({
388398
appID,
389399
constructFormState,
@@ -394,6 +404,29 @@ const FraudProtectionConfigurationScreen: React.VFC =
394404
const { selectedKey, onLinkClick, onChangeKey } =
395405
usePivotNavigation<FraudProtectionTab>(["overview", "logs", "settings"]);
396406

407+
const handleToggleEnabledAndSave = useCallback(
408+
(enabled: boolean) => {
409+
if (!enabled && form.state.enabled) {
410+
setShowDisableConfirmation(true);
411+
} else if (enabled && !form.state.enabled) {
412+
form.saveWith((_current) => ({
413+
...form.initialState,
414+
enabled: true,
415+
}));
416+
}
417+
},
418+
[form]
419+
);
420+
421+
const handleConfirmDisable = useCallback(() => {
422+
setShowDisableConfirmation(false);
423+
form.saveWith((_current) => ({ ...form.initialState, enabled: false }));
424+
}, [form]);
425+
426+
const handleCancelDisable = useCallback(() => {
427+
setShowDisableConfirmation(false);
428+
}, []);
429+
397430
if (form.isLoading || featureConfig.isLoading) {
398431
return <ShowLoading />;
399432
}
@@ -435,7 +468,35 @@ const FraudProtectionConfigurationScreen: React.VFC =
435468
selectedKey={selectedKey}
436469
onLinkClick={onLinkClick}
437470
onChangeKey={onChangeKey}
471+
onToggleEnabledAndSave={handleToggleEnabledAndSave}
438472
/>
473+
<Dialog
474+
hidden={!showDisableConfirmation}
475+
onDismiss={handleCancelDisable}
476+
dialogContentProps={{
477+
title: renderToString(
478+
"FraudProtectionConfigurationScreen.disable.confirmation.title"
479+
),
480+
subText: renderToString(
481+
"FraudProtectionConfigurationScreen.disable.confirmation.description"
482+
),
483+
}}
484+
>
485+
<DialogFooter>
486+
<DefaultButton
487+
onClick={handleCancelDisable}
488+
text={renderToString(
489+
"FraudProtectionConfigurationScreen.disable.confirmation.cancel"
490+
)}
491+
/>
492+
<PrimaryButton
493+
onClick={handleConfirmDisable}
494+
text={renderToString(
495+
"FraudProtectionConfigurationScreen.disable.confirmation.confirm"
496+
)}
497+
/>
498+
</DialogFooter>
499+
</Dialog>
439500
</FormContainer>
440501
);
441502
};

0 commit comments

Comments
 (0)