Skip to content

Commit

Permalink
feat(IT Wallet): [SIW-1802] Handle remotely disabled credentials and …
Browse files Browse the repository at this point in the history
…alert (#6442)

> [!WARNING]
> Depends on #6441 

## Short description
This PR handles the remote configuration for the credential selection
screen, in order to
- Display an alert to inform the user of any problems with credentials;
- Disable specific credentials so the user cannot start the issuance
flow.

## List of changes proposed in this pull request
- Modified `WalletCardOnboardingScreen`
- Created `ItwOnboardingModuleCredential` to keep the parent component
cleaner

## How to test
Mock the response of `/status/backend.json` with the following
configuration:
```json
{
  "statusMessages": {
    "items": [
      {
        "routes": ["ITW_CARD_ONBOARDING"],
        "level": "warning",
        "message": {
          "it-IT": "C'è un problema temporaneo sul servizio di emissione della versione digitale della Patente. Riprova più tardi.",
          "en-EN": "C'è un problema temporaneo sul servizio di emissione della versione digitale della Patente. Riprova più tardi."
        }
      }
    ]
  },
  "config": {
    "itw": {
      "disabled_credentials": ["MDL"]
    }
  }
}
```
<img
src="https://github.com/user-attachments/assets/f8d0c87a-2331-4ef5-8101-7cfe723a1e3d"
width="340" />

---------

Co-authored-by: Riccardo.Molinari <[email protected]>
Co-authored-by: RiccardoMolinari95 <[email protected]>
Co-authored-by: RiccardoMolinari95 <[email protected]>
Co-authored-by: Federico Mastrini <[email protected]>
  • Loading branch information
5 people authored Nov 26, 2024
1 parent 4136f6d commit bb7e753
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 64 deletions.
1 change: 1 addition & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3199,6 +3199,7 @@ features:
other: Altro
badge:
active: Già presente
unavailable: Non disponibile
options:
cgn: Carta Giovani Nazionale
welfare: Iniziative welfare
Expand Down
1 change: 1 addition & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3199,6 +3199,7 @@ features:
other: Altro
badge:
active: Già presente
unavailable: Non disponibile
options:
cgn: Carta Giovani Nazionale
welfare: Iniziative welfare
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useMemo, memo } from "react";
import { Badge, IOIcons, ModuleCredential } from "@pagopa/io-app-design-system";
import I18n from "../../../../i18n";
import { getCredentialNameFromType } from "../../common/utils/itwCredentialUtils";
import { CredentialType } from "../../common/utils/itwMocksUtils";

type Props = {
type: string;
onPress: (type: string) => void;
isActive: boolean;
isDisabled: boolean;
isCredentialIssuancePending: boolean;
isSelectedCredential: boolean;
};

const credentialIconByType: Record<string, IOIcons> = {
[CredentialType.DRIVING_LICENSE]: "car",
[CredentialType.EUROPEAN_DISABILITY_CARD]: "accessibility",
[CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD]: "healthCard"
};

const activeBadge: Badge = {
variant: "success",
text: I18n.t("features.wallet.onboarding.badge.active")
};

const disabledBadge: Badge = {
variant: "default",
text: I18n.t("features.wallet.onboarding.badge.unavailable")
};

const ItwOnboardingModuleCredential = ({
type,
onPress,
isActive,
isDisabled,
isSelectedCredential,
isCredentialIssuancePending
}: Props) => {
const badge = useMemo((): Badge | undefined => {
if (isActive) {
return activeBadge;
}
if (isDisabled) {
return disabledBadge;
}
return undefined;
}, [isActive, isDisabled]);

const handleOnPress = () => {
onPress(type);
};

const isPressable = !(isActive || isDisabled || isCredentialIssuancePending);

return (
<ModuleCredential
testID={`${type}ModuleTestID`}
icon={credentialIconByType[type]}
label={getCredentialNameFromType(type)}
onPress={isPressable ? handleOnPress : undefined}
isFetching={isCredentialIssuancePending && isSelectedCredential}
badge={badge}
/>
);
};

const MemoizedComponent = memo(ItwOnboardingModuleCredential);
export { MemoizedComponent as ItwOnboardingModuleCredential };
101 changes: 43 additions & 58 deletions ts/features/itwallet/onboarding/screens/WalletCardOnboardingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import {
Badge,
ContentWrapper,
IOIcons,
ListItemHeader,
ModuleCredential,
VStack
} from "@pagopa/io-app-design-system";
import { constFalse, pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import React from "react";
import React, { useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";
import { IOScrollViewWithLargeHeader } from "../../../../components/ui/IOScrollViewWithLargeHeader";
import I18n from "../../../../i18n";
import { useIONavigation } from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { isItwEnabledSelector } from "../../../../store/reducers/backendStatus/remoteConfig";
import {
isItwEnabledSelector,
itwDisabledCredentialsSelector
} from "../../../../store/reducers/backendStatus/remoteConfig";
import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp";
import { cgnActivationStart } from "../../../bonus/cgn/store/actions/activation";
import {
Expand All @@ -24,7 +26,6 @@ import {
import { loadAvailableBonuses } from "../../../bonus/common/store/actions/availableBonusesTypes";
import { PaymentsOnboardingRoutes } from "../../../payments/onboarding/navigation/routes";
import { isItwTrialActiveSelector } from "../../../trialSystem/store/reducers";
import { getCredentialNameFromType } from "../../common/utils/itwCredentialUtils";
import { CredentialType } from "../../common/utils/itwMocksUtils";
import { itwCredentialsTypesSelector } from "../../credentials/store/selectors";
import { itwLifecycleIsValidSelector } from "../../lifecycle/store/selectors";
Expand All @@ -38,6 +39,14 @@ import {
trackShowCredentialsList,
trackStartAddNewCredential
} from "../../analytics";
import { ItwOnboardingModuleCredential } from "../components/ItwOnboardingModuleCredential";

// List of available credentials to show to the user
const availableCredentials = [
CredentialType.DRIVING_LICENSE,
CredentialType.EUROPEAN_DISABILITY_CARD,
CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD
] as const;

const activeBadge: Badge = {
variant: "success",
Expand All @@ -49,7 +58,7 @@ const WalletCardOnboardingScreen = () => {
const isItwValid = useIOSelector(itwLifecycleIsValidSelector);
const isItwEnabled = useIOSelector(isItwEnabledSelector);

useFocusEffect(() => trackShowCredentialsList());
useFocusEffect(trackShowCredentialsList);

const isItwSectionVisible = React.useMemo(
// IT Wallet credential catalog should be visible if
Expand Down Expand Up @@ -79,6 +88,9 @@ const WalletCardOnboardingScreen = () => {

const ItwCredentialOnboardingSection = () => {
const machineRef = ItwCredentialIssuanceMachineContext.useActorRef();
const remotelyDisabledCredentials = useIOSelector(
itwDisabledCredentialsSelector
);

const isCredentialIssuancePending =
ItwCredentialIssuanceMachineContext.useSelector(selectIsLoading);
Expand All @@ -87,66 +99,39 @@ const ItwCredentialOnboardingSection = () => {

const itwCredentialsTypes = useIOSelector(itwCredentialsTypesSelector);

const beginCredentialIssuance = (type: CredentialType) => () => {
if (isCredentialIssuancePending) {
return;
}
const credentialName = CREDENTIALS_MAP[type];
trackStartAddNewCredential(credentialName);
machineRef.send({
type: "select-credential",
credentialType: type,
skipNavigation: true
});
};
// List of available credentials to show to the user
const availableCredentials = [
CredentialType.DRIVING_LICENSE,
CredentialType.EUROPEAN_DISABILITY_CARD,
CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD
] as const;

const credentialIconByType: Record<
(typeof availableCredentials)[number],
IOIcons
> = {
[CredentialType.DRIVING_LICENSE]: "car",
[CredentialType.EUROPEAN_DISABILITY_CARD]: "accessibility",
[CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD]: "healthCard"
};
const beginCredentialIssuance = useCallback(
(type: string) => {
trackStartAddNewCredential(CREDENTIALS_MAP[type]);
machineRef.send({
type: "select-credential",
credentialType: type,
skipNavigation: true
});
},
[machineRef]
);

return (
<>
<ListItemHeader
label={I18n.t("features.wallet.onboarding.sections.itw")}
/>
<VStack space={8}>
{availableCredentials.map(type => {
const isCredentialAlreadyActive = itwCredentialsTypes.includes(type);

return (
<ModuleCredential
key={`itw_credential_${type}`}
testID={`${type}ModuleTestID`}
icon={credentialIconByType[type]}
label={getCredentialNameFromType(type)}
onPress={
!isCredentialAlreadyActive
? beginCredentialIssuance(type)
: undefined
}
isFetching={
isCredentialIssuancePending &&
pipe(
selectedCredentialOption,
O.map(t => t === type),
O.getOrElse(constFalse)
)
}
badge={isCredentialAlreadyActive ? activeBadge : undefined}
/>
);
})}
{availableCredentials.map(type => (
<ItwOnboardingModuleCredential
key={`itw_credential_${type}`}
type={type}
isActive={itwCredentialsTypes.includes(type)}
isDisabled={remotelyDisabledCredentials.includes(type)}
isCredentialIssuancePending={isCredentialIssuancePending}
isSelectedCredential={pipe(
selectedCredentialOption,
O.map(t => t === type),
O.getOrElse(constFalse)
)}
onPress={beginCredentialIssuance}
/>
))}
</VStack>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,9 @@ type RenderOptions = {
isItwEnabled?: boolean;
isItwTestEnabled?: boolean;
itwLifecycle?: ItwLifecycleState;
remotelyDisabledCredentials?: Array<string>;
};

jest.mock("../../../../../config", () => ({
itwEnabled: true
}));

describe("WalletCardOnboardingScreen", () => {
it("it should render the screen correctly", () => {
const component = renderComponent({});
Expand All @@ -47,6 +44,11 @@ describe("WalletCardOnboardingScreen", () => {
expect(
queryByTestId(`${CredentialType.DRIVING_LICENSE}ModuleTestID`)
).toBeTruthy();
expect(
queryByTestId(
`${CredentialType.EUROPEAN_HEALTH_INSURANCE_CARD}ModuleTestID`
)
).toBeTruthy();
expect(
queryByTestId(`${CredentialType.EUROPEAN_DISABILITY_CARD}ModuleTestID`)
).toBeTruthy();
Expand All @@ -64,13 +66,30 @@ describe("WalletCardOnboardingScreen", () => {
expect(queryByTestId("itwDiscoveryBannerTestID")).toBeNull();
}
);

test.each([
{ remotelyDisabledCredentials: ["MDL"] },
{ remotelyDisabledCredentials: ["MDL", "EuropeanHealthInsuranceCard"] }
] as ReadonlyArray<RenderOptions>)(
"it should hide credential modules when $remotelyDisabledCredentials are remotely disabled",
options => {
const { queryByTestId } = renderComponent(options);
for (const type of options.remotelyDisabledCredentials!) {
// Currently ModuleCredential does not attach the testID if onPress is undefined.
// Since disabled credentials have undefined onPress, we can test for null.
expect(queryByTestId(`${type}ModuleTestID`)).toBeNull();
}
expect(queryByTestId("EuropeanDisabilityCardModuleTestID")).toBeTruthy();
}
);
});

const renderComponent = ({
isIdPayEnabled = true,
isItwEnabled = true,
itwTrialStatus = SubscriptionStateEnum.ACTIVE,
itwLifecycle = ItwLifecycleState.ITW_LIFECYCLE_VALID
itwLifecycle = ItwLifecycleState.ITW_LIFECYCLE_VALID,
remotelyDisabledCredentials
}: RenderOptions) => {
const globalState = appReducer(undefined, applicationChangeState("active"));

Expand Down Expand Up @@ -100,7 +119,8 @@ const renderComponent = ({
min_app_version: {
android: "0.0.0.0",
ios: "0.0.0.0"
}
},
disabled_credentials: remotelyDisabledCredentials
},
idPay: isIdPayEnabled && {
min_app_version: {
Expand Down
13 changes: 13 additions & 0 deletions ts/store/reducers/backendStatus/remoteConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,16 @@ export const isItwActivationDisabledSelector = createSelector(
O.getOrElse(() => false)
)
);

/**
* Return IT Wallet credentials that have been disabled remotely.
*/
export const itwDisabledCredentialsSelector = createSelector(
remoteConfigSelector,
remoteConfig =>
pipe(
remoteConfig,
O.chainNullableK(config => config.itw.disabled_credentials),
O.getOrElse(() => emptyArray)
)
);

0 comments on commit bb7e753

Please sign in to comment.