Skip to content

Commit

Permalink
feat: [LLK-49] Add section status banner for unsupported devices (pag…
Browse files Browse the repository at this point in the history
…opa#4405)

## Short description
This PR adds a local status banner that will be visible to users whose
devices do not support a **secure** hardware-based encryption key
storage. The banner will inform these users that they will not be able
to use future versions of IO on those devices.

See this
[RFC](https://pagopa.atlassian.net/wiki/spaces/IOAPP/pages/642318620/RFC-0003+Banner+in+app+per+notificare+gli+utenti+possessori+di+device+non+supportati)
for more insight.

| it | en | 
| - | - |
| <img
src="https://user-images.githubusercontent.com/16268789/222101528-22b06767-3a24-4074-8604-38ddab1d2a40.PNG"
width="150" /> | <img
src="https://user-images.githubusercontent.com/16268789/222101699-cc948966-2cd4-4250-b010-bae6a520a9f1.PNG"
width="150" /> |

## List of changes proposed in this pull request
- ts/components/SectionStatus/index.tsx: exports `InnerSectionComponent`
- ts/screens/messages/MessagesHomeScreen.tsx: adds unsupported device
banner shown if no key is found.

## How to test
- Verify that the banner is not displayed when Lollipop is disabled,
regardless of whether a public key is present or not.
- Confirm that the banner is not displayed when Lollipop is enabled and
a public key is present.
- Ensure that the banner is displayed when Lollipop is enabled and a
public key is not present.
- Ensure that by tapping on the banner you navigate to the link proposed
[here](https://pagopa.atlassian.net/browse/LLK-52).

---------

Co-authored-by: Cristiano Tofani <[email protected]>
Co-authored-by: Mario Perrotta <[email protected]>
Co-authored-by: Alessandro Dell'Oste <[email protected]>
  • Loading branch information
4 people authored Mar 2, 2023
1 parent 019aa36 commit c61f288
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 27 deletions.
4 changes: 2 additions & 2 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ PN_ENABLED=YES
REMINDERS_OPT_IN_ENABLED=YES
# FCI (Firma con IO) feature
FCI_ENABLED=YES
# LOLLIPOP login
LOLLIPOP_LOGIN_ENABLED=YES
# IDPay
IDPAY_ENABLED=YES
# IDPay RESTful API
IDPAY_API_BASEURL='https://api-io.cstar.pagopa.it'
# IDPay test/env RESTful API
IDPAY_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it'
# Unsupported device more information url
UNSUPPORTED_DEVICE_MORE_INFO_URL='https://io.italia.it/app-content/unsupported_device.html'
4 changes: 2 additions & 2 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ PN_ENABLED=YES
REMINDERS_OPT_IN_ENABLED=YES
# FCI (Firma con IO) feature
FCI_ENABLED=YES
# LOLLIPOP login
LOLLIPOP_LOGIN_ENABLED=NO
# IDPay
IDPAY_ENABLED=YES
# IDPay RESTful API
IDPAY_API_BASEURL='https://api-io.cstar.pagopa.it'
# IDPay test/env RESTful API
IDPAY_API_UAT_BASEURL='https://api-io.uat.cstar.pagopa.it'
# Unsupported device more information url
UNSUPPORTED_DEVICE_MORE_INFO_URL='https://io.italia.it/app-content/unsupported_device.html'
2 changes: 2 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3466,3 +3466,5 @@ idpay:
button:
addNew: Aggiungi nuovo
updateToast: IBAN associato correttamente
unsupportedDevice:
text: "Your device may soon no longer be compatible with the IO app."
2 changes: 2 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3489,3 +3489,5 @@ idpay:
button:
addNew: Aggiungi nuovo
updateToast: IBAN associato correttamente
unsupportedDevice:
text: "A breve il tuo dispositivo potrebbe non essere più compatibile con l’app IO."
2 changes: 1 addition & 1 deletion ts/components/SectionStatus/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const getStatusTextColor = (
): "bluegreyDark" | "white" =>
level === LevelEnum.normal ? "bluegreyDark" : textDefaultColor;

const InnerSectionStatus = (
export const InnerSectionStatus = (
props: Omit<Props, "sectionStatus"> & {
sectionStatus: NonNullable<Props["sectionStatus"]>;
}
Expand Down
9 changes: 6 additions & 3 deletions ts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,6 @@ export const newTransactionSummaryEnabled =
// FCI (Firma con IO) Feature Flag
export const fciEnabled = Config.FCI_ENABLED === "YES";

// LOLLIPOP login
export const lollipopLoginEnabled = Config.LOLLIPOP_LOGIN_ENABLED === "YES";

// PN (Piattaforma Notifiche) Feature Flag
export const pnEnabled = Config.PN_ENABLED === "YES";

Expand Down Expand Up @@ -190,6 +187,12 @@ export const localServicesWebUrl: string = pipe(
E.getOrElse(() => "https://io.italia.it")
);

export const unsupportedDeviceMoreInfoUrl: string = pipe(
Config.UNSUPPORTED_DEVICE_MORE_INFO_URL,
NonEmptyString.decode,
E.getOrElse(() => "https://io.italia.it/app-content/unsupported_device.html")
);

export const pageSize: number = DEFAULT_PAGE_SIZE;

// This is the maximum number supported by API via pagination regardless of the content.
Expand Down
4 changes: 1 addition & 3 deletions ts/features/lollipop/hooks/useLollipopLoginSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";
import { useCallback, useEffect, useState } from "react";
import { lollipopLoginEnabled } from "../../../config";
import { useIOSelector } from "../../../store/hooks";
import { isLollipopEnabledSelector } from "../../../store/reducers/backendStatus";
import { trackLollipopIdpLoginFailure } from "../../../utils/analytics";
Expand All @@ -16,8 +15,7 @@ export const useLollipopLoginSource = (loginUri?: string) => {
kind: "initial"
});

const useLollipopLogin =
useIOSelector(isLollipopEnabledSelector) && lollipopLoginEnabled;
const useLollipopLogin = useIOSelector(isLollipopEnabledSelector);
const lollipopKeyTag = useIOSelector(lollipopKeyTagSelector);

const setDeprecatedLoginUri = useCallback((uri: string) => {
Expand Down
42 changes: 42 additions & 0 deletions ts/features/lollipop/hooks/usePublicKeyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PublicKey } from "@pagopa/io-react-native-crypto";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";
import { useEffect, useState } from "react";
import { useIOSelector } from "../../../store/hooks";
import { taskGetPublicKey } from "../../../utils/crypto";
import { lollipopKeyTagSelector } from "../store/reducers/lollipop";

type PKReady = { kind: "ready"; publicKey: PublicKey };
type PKChecking = { kind: "checking" };
type PKError = { kind: "error"; error: string };
type PKStatus = PKReady | PKChecking | PKError;

export const usePublicKeyState = () => {
const [publicKeyState, setPublicKeyState] = useState<PKStatus>({
kind: "checking"
});
const keyTag = useIOSelector(lollipopKeyTagSelector);

const handleError = (error: string) =>
setPublicKeyState({ kind: "error", error });

useEffect(() => {
pipe(
keyTag,
O.fold(
() => handleError("Missing key tag"),
tag =>
pipe(
tag,
taskGetPublicKey,
TE.map(publicKey =>
setPublicKeyState({ kind: "ready", publicKey })
),
TE.mapLeft(e => handleError(e.message))
)()
)
);
}, [keyTag]);
return publicKeyState;
};
27 changes: 21 additions & 6 deletions ts/features/lollipop/saga/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import * as O from "fp-ts/lib/Option";
import { v4 as uuid } from "uuid";
import { put, select } from "typed-redux-saga/macro";
import { put, select, call } from "typed-redux-saga/macro";
import { getPublicKey } from "@pagopa/io-react-native-crypto";
import { lollipopKeyTagSelector } from "../store/reducers/lollipop";
import { lollipopKeyTagSave } from "../store/actions/lollipop";
import {
cryptoKeyGenerationSaga,
deletePreviousCryptoKeyPair
} from "../../../sagas/startup/generateCryptoKeyPair";
import { lollipopLoginEnabled } from "../../../config";
import { isLollipopEnabledSelector } from "../../../store/reducers/backendStatus";

export function* generateLollipopKeySaga() {
const isLollipopEnabled = yield* select(isLollipopEnabledSelector);
if (!isLollipopEnabled) {
return;
}
const maybeOldKeyTag = yield* select(lollipopKeyTagSelector);
// Weather the user is logged in or not
// we generate a key (if no one is present)
Expand All @@ -19,10 +24,20 @@ export function* generateLollipopKeySaga() {
const newKeyTag = uuid();
yield* put(lollipopKeyTagSave({ keyTag: newKeyTag }));
yield* cryptoKeyGenerationSaga(newKeyTag, maybeOldKeyTag);
if (!lollipopLoginEnabled) {
// If the lollipop login is not enable we immediately delete
// the new generated key.
yield* deletePreviousCryptoKeyPair(O.some(newKeyTag));
} else {
try {
// If we already have a keyTag, we check if there is
// a public key tied with it.
yield* call(getPublicKey, maybeOldKeyTag.value);
} catch {
// If there is no key it could be for two reasons:
// - The user have a recent app and they logged out (the key is deleted).
// - The user is logged in and is updating from an app version
// that didn't manage the key generation.
// Having a key or an error in those cases is useful to show
// the user an informative banner saying that their device
// is not suitable for future version of IO.
yield* cryptoKeyGenerationSaga(maybeOldKeyTag.value, O.none);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions ts/sagas/__tests__/initializeApplicationSaga.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getPublicKey } from "@pagopa/io-react-native-crypto";
import * as O from "fp-ts/lib/Option";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { testSaga } from "redux-saga-test-plan";
Expand Down Expand Up @@ -30,6 +31,7 @@ import { watchSessionExpiredSaga } from "../startup/watchSessionExpiredSaga";
import { watchProfileEmailValidationChangedSaga } from "../watchProfileEmailValidationChangedSaga";
import { checkAppHistoryVersionSaga } from "../startup/appVersionHistorySaga";
import { generateLollipopKeySaga } from "../../features/lollipop/saga";
import { isLollipopEnabledSelector } from "../../store/reducers/backendStatus";

const aSessionToken = "a_session_token" as SessionToken;

Expand Down Expand Up @@ -73,6 +75,8 @@ describe("initializeApplicationSaga", () => {
.put(resetProfileState())
.next()
.next(generateLollipopKeySaga)
.next(isLollipopEnabledSelector)
.next(getPublicKey)
.select(sessionTokenSelector)
.next(aSessionToken)
.fork(watchSessionExpiredSaga)
Expand Down Expand Up @@ -107,6 +111,8 @@ describe("initializeApplicationSaga", () => {
.put(resetProfileState())
.next()
.next(generateLollipopKeySaga)
.next(isLollipopEnabledSelector)
.next(getPublicKey)
.select(sessionTokenSelector)
.next(aSessionToken)
.fork(watchSessionExpiredSaga)
Expand Down Expand Up @@ -138,6 +144,8 @@ describe("initializeApplicationSaga", () => {
.put(resetProfileState())
.next()
.next(generateLollipopKeySaga)
.next(isLollipopEnabledSelector)
.next(getPublicKey)
.select(sessionTokenSelector)
.next(aSessionToken)
.fork(watchSessionExpiredSaga)
Expand Down
39 changes: 36 additions & 3 deletions ts/screens/messages/MessagesHomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { connect } from "react-redux";
import { Dispatch } from "redux";

import { createSelector } from "reselect";
import { LevelEnum } from "../../../definitions/content/SectionStatus";
import { IOColors } from "../../components/core/variables/IOColors";
import { useMessageOpening } from "../../components/messages/hooks/useMessageOpening";
import MessageList from "../../components/messages/MessageList";
Expand All @@ -16,15 +17,23 @@ import { ScreenContentHeader } from "../../components/screens/ScreenContentHeade
import TopScreenComponent from "../../components/screens/TopScreenComponent";
import { MIN_CHARACTER_SEARCH_TEXT } from "../../components/search/SearchButton";
import { SearchNoResultMessage } from "../../components/search/SearchNoResultMessage";
import SectionStatusComponent from "../../components/SectionStatus";
import SectionStatusComponent, {
InnerSectionStatus
} from "../../components/SectionStatus";
import FocusAwareStatusBar from "../../components/ui/FocusAwareStatusBar";
import { unsupportedDeviceMoreInfoUrl } from "../../config";
import { usePublicKeyState } from "../../features/lollipop/hooks/usePublicKeyState";
import I18n from "../../i18n";
import MessagesHomeTabNavigator from "../../navigation/MessagesHomeTabNavigator";
import {
migrateToPaginatedMessages,
resetMigrationStatus
} from "../../store/actions/messages";
import { sectionStatusSelector } from "../../store/reducers/backendStatus";
import { useIOSelector } from "../../store/hooks";
import {
isLollipopEnabledSelector,
sectionStatusSelector
} from "../../store/reducers/backendStatus";
import {
allArchiveMessagesSelector,
allInboxMessagesSelector,
Expand Down Expand Up @@ -75,6 +84,8 @@ const MessagesHomeScreen = ({
}: Props) => {
const needsMigration = Object.keys(messagesStatus).length > 0;

const publicKeyState = usePublicKeyState();

useOnFirstRender(() => {
if (needsMigration) {
migrateMessages(messagesStatus);
Expand Down Expand Up @@ -110,6 +121,27 @@ const MessagesHomeScreen = ({

const isScreenReaderEnabled = useScreenReaderEnabled();

const isLollipopEnabled = useIOSelector(isLollipopEnabledSelector);
const showUnsupportedDeviceBanner =
isLollipopEnabled && publicKeyState.kind === "error";
const unsupportedDevicesStatusComponent = showUnsupportedDeviceBanner && (
<InnerSectionStatus
sectionKey={"messages"}
sectionStatus={{
is_visible: true,
level: LevelEnum.warning,
web_url: {
"it-IT": unsupportedDeviceMoreInfoUrl,
"en-EN": unsupportedDeviceMoreInfoUrl
},
message: {
"it-IT": I18n.t("unsupportedDevice.text"),
"en-EN": I18n.t("unsupportedDevice.text")
}
}}
/>
);

const statusComponent = (
<SectionStatusComponent
sectionKey={"messages"}
Expand All @@ -136,6 +168,7 @@ const MessagesHomeScreen = ({
backgroundColor={IOColors.white}
/>
{isScreenReaderEnabled && statusComponent}
{isScreenReaderEnabled && unsupportedDevicesStatusComponent}
{!isSearchEnabled && (
<React.Fragment>
<ScreenContentHeader
Expand All @@ -153,7 +186,6 @@ const MessagesHomeScreen = ({
)}
</React.Fragment>
)}

{isSearchEnabled &&
pipe(
searchText,
Expand All @@ -179,6 +211,7 @@ const MessagesHomeScreen = ({
))
)}
{!isScreenReaderEnabled && statusComponent}
{!isScreenReaderEnabled && unsupportedDevicesStatusComponent}
{bottomSheet}
</TopScreenComponent>
);
Expand Down
8 changes: 1 addition & 7 deletions ts/utils/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,7 @@ export const taskRegenerateKey = (keyTag: string) =>
);

export const taskGetPublicKey = (keyTag: string) =>
pipe(
TE.tryCatch(
() => getPublicKey(keyTag),
() => undefined
),
TE.getOrElseW(() => T.of(undefined))
)();
pipe(TE.tryCatch(() => getPublicKey(keyTag), toCryptoError));

export const taskGeneratePublicKey = (keyTag: string) =>
pipe(
Expand Down

0 comments on commit c61f288

Please sign in to comment.