From 6e56bac02ad2b04cbe92adbf9c91ab251124210e Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 22 Jan 2024 14:09:04 +0100 Subject: [PATCH 1/9] chore: [IOBP-510] Add `UIWalletDetailsInfo` to bypass `WalletDetailsInfo`'s `type` enum. (#5412) ## Short description This PR introduces `UIWalletInfoDetails` type, which removes the dependency from the `type` prop in `WalletInfoDetails` ## List of changes proposed in this pull request - Added `UIWalletInfoDetails` - UI refactoring to remove `type` props dependency - Update API definitions for wallet and payments ## How to test With the `io-dev-api-server`, try to: - onboard a new payment method - start a new payment flow --------- Co-authored-by: Alessandro Izzo Co-authored-by: Alessandro Izzo --- package.json | 4 +- .../WalletDetailsPaymentMethodFeatures.tsx | 2 +- ts/features/walletV3/common/utils/index.ts | 72 +++++++++---------- .../details/screens/WalletDetailsScreen.tsx | 69 ++++++++---------- .../details/types/UIWalletInfoDetails.ts | 40 +++++++++++ .../WalletPaymentConfirmContent.tsx | 72 ++++++++----------- .../handleWalletPaymentAuthorization.ts | 14 ++-- .../screens/WalletPaymentPickMethodScreen.tsx | 46 ++++++------ 8 files changed, 166 insertions(+), 153 deletions(-) create mode 100644 ts/features/walletV3/details/types/UIWalletInfoDetails.ts diff --git a/package.json b/package.json index fd4d0b1467a..8c95c272193 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "idpay_api": "https://raw.githubusercontent.com/pagopa/cstar-infrastructure/v6.9.1/src/domains/idpay-app/api/idpay_appio_full/openapi.appio.full.yml", "lollipop_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.25.1-RELEASE/api_lollipop_first_consumer.yaml", "fast_login_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.25.1-RELEASE/openapi/generated/api_fast_login.yaml", - "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/4cd111e94432ff62580adc391de78a5462a7128e/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", - "pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/65878f9913fcc0eaff499ba8a1a20427a412c010/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", + "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/740e7dcc5ea2ea19639316fea6797bbd504dd0ae/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", + "pagopa_api_ecommerce": "https://raw.githubusercontent.com/pagopa/pagopa-infra/5190135ac34791cf66c1986735d4134bcaf4096f/src/domains/ecommerce-app/api/ecommerce-io/v1/_openapi.json.tpl", "private": true, "scripts": { "start": "react-native start", diff --git a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx b/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx index 959c7e90a41..b3799b6f200 100644 --- a/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx +++ b/ts/features/walletV3/common/components/WalletDetailsPaymentMethodFeatures.tsx @@ -19,7 +19,7 @@ type Props = { paymentMethod: WalletInfo }; * - global settings (payment capability, favourite, etc.) */ const WalletDetailsPaymentMethodFeatures = ({ paymentMethod }: Props) => { - const isMethodExpired = isPaymentMethodExpired(paymentMethod); + const isMethodExpired = isPaymentMethodExpired(paymentMethod.details); const isIdpayEnabled = useIOSelector(isIdPayEnabledSelector); if (isMethodExpired) { diff --git a/ts/features/walletV3/common/utils/index.ts b/ts/features/walletV3/common/utils/index.ts index 67866723415..812c3a3b7b6 100644 --- a/ts/features/walletV3/common/utils/index.ts +++ b/ts/features/walletV3/common/utils/index.ts @@ -1,25 +1,20 @@ -import _ from "lodash"; -import * as O from "fp-ts/lib/Option"; import { IOLogoPaymentType, IOPaymentLogos, ListItemTransactionStatusWithBadge } from "@pagopa/io-app-design-system"; -import I18n from "i18n-js"; +import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; - -import { isExpiredDate } from "../../../../utils/dates"; +import I18n from "i18n-js"; +import _ from "lodash"; +import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; import { ServiceNameEnum } from "../../../../../definitions/pagopa/walletv3/ServiceName"; -import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; import { ServiceStatusEnum } from "../../../../../definitions/pagopa/walletv3/ServiceStatus"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; +import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; +import { isExpiredDate } from "../../../../utils/dates"; import { findFirstCaseInsensitive } from "../../../../utils/object"; -import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; import { WalletPaymentPspSortType } from "../../payment/types"; /** @@ -50,16 +45,15 @@ export const getBadgeTextByTransactionStatus = ( * left if expiring date can't be evaluated * @param paymentMethod */ -export const isPaymentMethodExpired = (paymentMethod: WalletInfo): boolean => { - switch (paymentMethod.details?.type) { - case TypeEnum.PAYPAL: - return false; - case TypeEnum.CARDS: - const cardDetails = paymentMethod.details as WalletInfoDetails1; - return isExpiredDate(cardDetails.expiryDate); - } - return false; -}; +export const isPaymentMethodExpired = ( + details?: UIWalletInfoDetails +): boolean => + pipe( + details?.expiryDate, + O.fromNullable, + O.map(isExpiredDate), + O.getOrElse(() => false) + ); /** * true if the given paymentMethod supports the given walletFunction @@ -111,25 +105,25 @@ export const isPaymentSupported = ( }; export const getPaymentLogo = ( - selectedMethod: WalletInfoDetails + details: UIWalletInfoDetails ): IOLogoPaymentType | undefined => { - switch (selectedMethod.type) { - case TypeEnum.CARDS: - const cardsType = selectedMethod as WalletInfoDetails1; - const { brand } = cardsType; - return pipe( - brand, - findFirstCaseInsensitive(IOPaymentLogos), - O.fold( - () => undefined, - ([logoName, _]) => logoName - ) - ) as IOLogoPaymentType; - case TypeEnum.PAYPAL: - return "payPal"; - default: - return undefined; + if (details.maskedEmail !== undefined) { + return "payPal"; + } else if (details.maskedNumber !== undefined) { + return "bancomatPay"; + } else if (details.maskedPan !== undefined) { + return pipe( + details.brand, + O.fromNullable, + O.chain(findFirstCaseInsensitive(IOPaymentLogos)), + O.fold( + () => undefined, + ([logoName, _]) => logoName + ) + ) as IOLogoPaymentType; } + + return undefined; }; export const WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL = diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx index ea637521529..64c923cf7b8 100644 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx @@ -20,12 +20,7 @@ import { walletDetailsInstrumentSelector } from "../store"; import { walletDetailsGetInstrument } from "../store/actions"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1, - WalletInfoDetails2 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; +import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; export type WalletDetailsScreenNavigationParams = Readonly<{ walletId: string; @@ -36,44 +31,38 @@ export type WalletDetailsScreenRouteProps = RouteProp< "WALLET_DETAILS_SCREEN" >; -const generateCardComponent = (walletDetails: WalletInfoDetails) => { - switch (walletDetails.type) { - case TypeEnum.PAYPAL: - const paypalDetails = walletDetails as WalletInfoDetails2; - return ( - - ); - case TypeEnum.CARDS: - default: - const cardDetails = walletDetails as WalletInfoDetails1; - return ( - - ); +const generateCardComponent = (details: UIWalletInfoDetails) => { + if (details.maskedEmail !== undefined) { + return ( + + ); } + + return ( + + ); }; -const generateCardHeaderTitle = (walletDetails?: WalletInfoDetails) => { - switch (walletDetails?.type) { - case TypeEnum.CARDS: - const cardDetails = walletDetails as WalletInfoDetails1; - const capitalizedCardCircuit = capitalize( - cardDetails.brand.toLowerCase() ?? "" - ); - return `${capitalizedCardCircuit} ••${cardDetails.maskedPan}`; - default: - return ""; +const generateCardHeaderTitle = (details?: UIWalletInfoDetails) => { + if (details?.maskedPan !== undefined) { + const capitalizedCardCircuit = capitalize( + details.brand?.toLowerCase() ?? "" + ); + return `${capitalizedCardCircuit} ••${details.maskedPan}`; } + + return ""; }; /** diff --git a/ts/features/walletV3/details/types/UIWalletInfoDetails.ts b/ts/features/walletV3/details/types/UIWalletInfoDetails.ts new file mode 100644 index 00000000000..56f40ccbd47 --- /dev/null +++ b/ts/features/walletV3/details/types/UIWalletInfoDetails.ts @@ -0,0 +1,40 @@ +import * as t from "io-ts"; +import { + WalletInfoDetails1, + WalletInfoDetails2, + WalletInfoDetails3 +} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; + +/** + * Transforms all required props from WalletInfoDetails1 to partial + */ +export const UIWalletInfoDetails1 = t.partial({ + ...WalletInfoDetails1.types[0].props, + ...WalletInfoDetails1.types[0].props +}); + +/** + * Transforms all required props from WalletInfoDetails2 to partial + */ +export const UIWalletInfoDetails2 = t.partial({ + ...WalletInfoDetails2.types[0].props, + ...WalletInfoDetails2.types[1].props +}); + +/** + * Transforms all required props from WalletInfoDetails3 to partial + */ +export const UIWalletInfoDetails3 = t.partial({ + ...WalletInfoDetails3.types[0].props, + ...WalletInfoDetails3.types[1].props +}); + +/** + * This type is used to bypass the `type` props of {@see WalletInfoDetails} + */ +export const UIWalletInfoDetails = t.intersection( + [UIWalletInfoDetails1, UIWalletInfoDetails2, UIWalletInfoDetails3], + "UIWalletInfoDetails" +); + +export type UIWalletInfoDetails = t.TypeOf; diff --git a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx b/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx index d7c8a22c6d2..456914c71e8 100644 --- a/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx +++ b/ts/features/walletV3/payment/components/WalletPaymentConfirmContent.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Body, GradientScrollView, @@ -7,35 +6,29 @@ import { ModuleCheckout, VSpacer } from "@pagopa/io-app-design-system"; -import { useNavigation } from "@react-navigation/native"; import { openAuthenticationSession } from "@pagopa/io-react-native-login-utils"; +import { useNavigation } from "@react-navigation/native"; +import React from "react"; +import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; +import { PaymentRequestsGetResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; +import I18n from "../../../../i18n"; import { AppParamsList, IOStackNavigationProp } from "../../../../navigation/params/AppParamsList"; -import { WalletPaymentRoutes } from "../navigation/routes"; +import { format } from "../../../../utils/dates"; +import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { capitalize } from "../../../../utils/strings"; import { WALLET_PAYMENT_TERMS_AND_CONDITIONS_URL, getPaymentLogo } from "../../common/utils"; -import { format } from "../../../../utils/dates"; -import { capitalize } from "../../../../utils/strings"; -import { PaymentRequestsGetResponse } from "../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; -import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; -import { - TypeEnum, - WalletInfoDetails, - WalletInfoDetails1, - WalletInfoDetails2, - WalletInfoDetails3 -} from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; -import I18n from "../../../../i18n"; - -import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; +import { WalletPaymentRoutes } from "../navigation/routes"; import { WalletPaymentTotalAmount } from "./WalletPaymentTotalAmount"; export type WalletPaymentConfirmContentProps = { - paymentMethodDetails: WalletInfoDetails; + paymentMethodDetails: UIWalletInfoDetails; selectedPsp: Bundle; paymentDetails: PaymentRequestsGetResponse; isLoading?: boolean; @@ -127,33 +120,26 @@ export const WalletPaymentConfirmContent = ({ ); }; -const getPaymentSubtitle = (cardDetails: WalletInfoDetails) => { - switch (cardDetails.type) { - case TypeEnum.CARDS: - const cardsDetail = cardDetails as WalletInfoDetails1; - return `${format(cardsDetail.expiryDate, "MM/YY")}`; - case TypeEnum.PAYPAL: - return I18n.t("wallet.onboarding.paypal.name"); - case TypeEnum.BANCOMATPAY: - const bancomatpayDetail = cardDetails as WalletInfoDetails3; - return `${bancomatpayDetail.bankName}`; - default: - return ""; +const getPaymentSubtitle = (details: UIWalletInfoDetails): string => { + if (details.maskedPan !== undefined) { + return `${format(details.expiryDate, "MM/YY")}`; + } else if (details.maskedEmail !== undefined) { + return I18n.t("wallet.onboarding.paypal.name"); + } else if (details.maskedNumber !== undefined) { + return `${details.bankName}`; } + + return ""; }; -const getPaymentTitle = (cardDetails: WalletInfoDetails) => { - switch (cardDetails.type) { - case TypeEnum.CARDS: - const cardsDetail = cardDetails as WalletInfoDetails1; - return `${capitalize(cardsDetail.brand)} ••${cardsDetail.maskedPan}`; - case TypeEnum.PAYPAL: - const paypalDetail = cardDetails as WalletInfoDetails2; - return `${paypalDetail.maskedEmail}`; - case TypeEnum.BANCOMATPAY: - const bancomatpayDetail = cardDetails as WalletInfoDetails3; - return `${bancomatpayDetail.maskedNumber}`; - default: - return ""; +const getPaymentTitle = (details: UIWalletInfoDetails): string => { + if (details.maskedPan !== undefined) { + return `${capitalize(details.brand || "")} ••${details.maskedPan}`; + } else if (details.maskedEmail !== undefined) { + return `${details.maskedEmail}`; + } else if (details.maskedNumber !== undefined) { + return `${details.maskedNumber}`; } + + return ""; }; diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts index d634e782503..2a1a9614566 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts +++ b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentAuthorization.ts @@ -2,16 +2,17 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import { call, put } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; +import { + LanguageEnum, + RequestAuthorizationRequest +} from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationRequest"; +import { WalletDetailTypeEnum } from "../../../../../../definitions/pagopa/ecommerce/WalletDetailType"; import { SagaCallReturnType } from "../../../../../types/utils"; import { getGenericError, getNetworkError } from "../../../../../utils/errors"; import { readablePrivacyReport } from "../../../../../utils/reporters"; import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; import { PaymentClient } from "../../api/client"; import { walletPaymentAuthorization } from "../../store/actions/networking"; -import { - LanguageEnum, - RequestAuthorizationRequest -} from "../../../../../../definitions/pagopa/ecommerce/RequestAuthorizationRequest"; export function* handleWalletPaymentAuthorization( requestTransactionAuthorization: PaymentClient["requestTransactionAuthorization"], @@ -23,7 +24,10 @@ export function* handleWalletPaymentAuthorization( isAllCCP: true, language: LanguageEnum.IT, pspId: action.payload.pspId, - walletId: action.payload.walletId + details: { + detailType: WalletDetailTypeEnum.wallet, + walletId: action.payload.walletId + } }; const requestTransactionAuthorizationRequest = requestTransactionAuthorization({ diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx index 23c53a5caa0..bed3a5835dc 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx @@ -20,7 +20,6 @@ import React, { useEffect, useMemo } from "react"; import { View } from "react-native"; import { PaymentMethodResponse } from "../../../../../definitions/pagopa/walletv3/PaymentMethodResponse"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { WalletInfoDetails1 } from "../../../../../definitions/pagopa/walletv3/WalletInfoDetails"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { @@ -43,6 +42,7 @@ import { } from "../store/selectors"; import { WalletPaymentMissingMethodsError } from "../components/WalletPaymentMissingMethodsError"; import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler"; +import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; type SavedMethodState = { kind: "saved"; @@ -265,29 +265,29 @@ const mapGenericToRadioItem = ( const mapSavedToRadioItem = ( method: WalletInfo ): RadioItem | undefined => { - switch (method.details?.type) { - case "CARDS": - const cardDetails = method.details as WalletInfoDetails1; - return { - id: method.walletId, - value: `${capitalize(cardDetails.brand)} ••${cardDetails.maskedPan}`, - startImage: getIconWithFallback(cardDetails.brand) - }; - case "PAYPAL": - return { - id: method.walletId, - value: "PayPal", - startImage: getIconWithFallback("paypal") - }; - case "BANCOMATPAY": - return { - id: method.walletId, - value: "BANCOMAT Pay", - startImage: getIconWithFallback("bancomatpay") - }; - default: - return undefined; + const details = method.details as UIWalletInfoDetails; + + if (details.maskedPan !== undefined) { + return { + id: method.walletId, + value: `${capitalize(details.brand)} ••${details.maskedPan}`, + startImage: getIconWithFallback(details.brand) + }; + } else if (details.maskedEmail !== undefined) { + return { + id: method.walletId, + value: "PayPal", + startImage: getIconWithFallback("paypal") + }; + } else if (details.maskedNumber !== undefined) { + return { + id: method.walletId, + value: "BANCOMAT Pay", + startImage: getIconWithFallback("bancomatpay") + }; } + + return undefined; }; const isMethodDisabledForAmount = ( From 3431429fa9d6f7ee5d92fd814af4e9d365c71647 Mon Sep 17 00:00:00 2001 From: Alessandro Izzo Date: Mon, 22 Jan 2024 22:10:14 +0100 Subject: [PATCH 2/9] fix: [IOBP-515] Expiry date into new wallet details screen (#5418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ⚠️ This PR depends on #5412 ⚠️ ## Short description This PR fixes the expiry date inside the new wallet details screen showing the correct date. ## List of changes proposed in this pull request - Added an additional util function `getDateFromExpiryDate` which returns a Date object from a string in format "YYYYMM" - Fixed the `isExpiredDate` utility function with the right string slicing; ## How to test Complete a new onboarding process from the playground and at the end you should be able to see the correct expiry date ## Preview |Before|After| |-|-| | | | --------- Co-authored-by: Federico Mastrini --- .../details/screens/WalletDetailsScreen.tsx | 3 ++- ts/utils/dates.ts | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx index 64c923cf7b8..2eec49f09b4 100644 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx @@ -21,6 +21,7 @@ import { } from "../store"; import { walletDetailsGetInstrument } from "../store/actions"; import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; +import { getDateFromExpiryDate } from "../../../../utils/dates"; export type WalletDetailsScreenNavigationParams = Readonly<{ walletId: string; @@ -46,7 +47,7 @@ const generateCardComponent = (details: UIWalletInfoDetails) => { { - const year = +expiryDate.slice(3, 5); - const month = +expiryDate.slice(0, 3); + const year = +expiryDate.slice(0, 4); + const month = +expiryDate.slice(4, 6); const now = new Date(); const nowYearMonth = new Date(now.getFullYear(), now.getMonth() + 1); const cardExpirationDate = new Date(year, month); @@ -320,3 +320,13 @@ export const toAndroidCacheTimestamp = () => new Date(), I18n.t("global.dateFormats.shortFormat").replace(/\//g, "") ); + +/** + * This function returns a Date object from a string in format "YYYYMM" + * @param expiryDate + */ +export const getDateFromExpiryDate = (expiryDate: string): Date => { + const year = +expiryDate.slice(0, 4); + const month = +expiryDate.slice(4, 6); + return new Date(year, month - 1); +}; From 46af86615b881d57a9f785092eca3baf0cc89ada Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Tue, 23 Jan 2024 09:47:54 +0100 Subject: [PATCH 3/9] chore: [IOPID-1301] Bump ToS version from 4.7 to 4.8"" (#5402) Reverts pagopa/io-app#5391 --- ts/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/config.ts b/ts/config.ts index a5496e6722c..92ede32ee6c 100644 --- a/ts/config.ts +++ b/ts/config.ts @@ -122,7 +122,7 @@ export const remindersOptInEnabled = Config.REMINDERS_OPT_IN_ENABLED === "YES"; export const isNewCduFlow = Config.CDU_NEW_FLOW === "YES"; // version of ToS -export const tosVersion: NonNegativeNumber = 4.7 as NonNegativeNumber; +export const tosVersion: NonNegativeNumber = 4.8 as NonNegativeNumber; export const fetchTimeout = pipe( parseInt(Config.FETCH_TIMEOUT_MS, 10), From 9fc529f943ab7854f84980655e0064c77c699f33 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:47:59 +0100 Subject: [PATCH 4/9] feat: [IOPID-1391] Fix tablet compatibility alert displaying (#5417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short description This PR fixes the continuously displaying of the Tablet compatibility alert during the login flow. It took the chance to refactor the `LandingScreen` from `PureComponent` to `FunctionComponent`.
Details

| ❌ | ✅ | | - | - | |

## How to test Launch the app on an Android Tablet and check that the Alert is shown only at every first render of `LandingScreen`. --- ts/navigation/AuthenticationNavigator.tsx | 2 +- ts/screens/authentication/LandingScreen.tsx | 320 ++++++++++---------- 2 files changed, 158 insertions(+), 164 deletions(-) diff --git a/ts/navigation/AuthenticationNavigator.tsx b/ts/navigation/AuthenticationNavigator.tsx index c7a392f824e..af41d43c234 100644 --- a/ts/navigation/AuthenticationNavigator.tsx +++ b/ts/navigation/AuthenticationNavigator.tsx @@ -10,7 +10,7 @@ import CiePinScreen from "../screens/authentication/cie/CiePinScreen"; import CieWrongCiePinScreen from "../screens/authentication/cie/CieWrongCiePinScreen"; import IdpLoginScreen from "../screens/authentication/IdpLoginScreen"; import IdpSelectionScreen from "../screens/authentication/IdpSelectionScreen"; -import LandingScreen from "../screens/authentication/LandingScreen"; +import { LandingScreen } from "../screens/authentication/LandingScreen"; import TestAuthenticationScreen from "../screens/authentication/TestAuthenticationScreen"; import MarkdownScreen from "../screens/development/MarkdownScreen"; import { AuthSessionPage } from "../screens/authentication/idpAuthSessionHandler"; diff --git a/ts/screens/authentication/LandingScreen.tsx b/ts/screens/authentication/LandingScreen.tsx index 4f138840e7f..3053331215e 100644 --- a/ts/screens/authentication/LandingScreen.tsx +++ b/ts/screens/authentication/LandingScreen.tsx @@ -10,8 +10,9 @@ import { Content, Text as NBButtonText } from "native-base"; import * as React from "react"; import { View, Alert, StyleSheet } from "react-native"; import DeviceInfo from "react-native-device-info"; -import { connect } from "react-redux"; +import { useDispatch, useStore } from "react-redux"; import { IOColors, Icon, HSpacer, VSpacer } from "@pagopa/io-app-design-system"; +import { useNavigation } from "@react-navigation/native"; import sessionExpiredImg from "../../../img/landing/session_expired.png"; import ButtonDefaultOpacity from "../../components/ButtonDefaultOpacity"; import CieNotSupported from "../../components/cie/CieNotSupported"; @@ -19,7 +20,6 @@ import ContextualInfo from "../../components/ContextualInfo"; import { Link } from "../../components/core/typography/Link"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { DevScreenButton } from "../../components/DevScreenButton"; -import { withLightModalContext } from "../../components/helpers/withLightModalContext"; import { HorizontalScroll } from "../../components/HorizontalScroll"; import { renderInfoRasterImage } from "../../components/infoScreen/imageRendering"; import { InfoScreenComponent } from "../../components/infoScreen/InfoScreenComponent"; @@ -29,28 +29,19 @@ import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; import SectionStatusComponent from "../../components/SectionStatus"; -import { LightModalContextInterface } from "../../components/ui/LightModal"; import I18n from "../../i18n"; import { mixpanelTrack } from "../../mixpanel"; -import { - AppParamsList, - IOStackNavigationRouteProps -} from "../../navigation/params/AppParamsList"; import ROUTES from "../../navigation/routes"; import { idpSelected, resetAuthenticationState } from "../../store/actions/authentication"; -import { continueWithRootOrJailbreak } from "../../store/actions/persistedPreferences"; -import { Dispatch } from "../../store/actions/types"; -import { isSessionExpiredSelector } from "../../store/reducers/authentication"; import { hasApiLevelSupportSelector, hasNFCFeatureSelector, isCieSupportedSelector } from "../../store/reducers/cie"; import { continueWithRootOrJailbreakSelector } from "../../store/reducers/persistedPreferences"; -import { GlobalState } from "../../store/reducers/types"; import variables from "../../theme/variables"; import { ComponentProps } from "../../types/react"; import { isDevEnv } from "../../utils/environment"; @@ -64,24 +55,17 @@ import { } from "../../features/fastLogin/store/selectors"; import { isCieLoginUatEnabledSelector } from "../../features/cieLogin/store/selectors"; import { cieFlowForDevServerEnabled } from "../../features/cieLogin/utils"; +import { useOnFirstRender } from "../../utils/hooks/useOnFirstRender"; +import { useIOSelector } from "../../store/hooks"; +import { isSessionExpiredSelector } from "../../store/reducers/authentication"; +import { LightModalContext } from "../../components/ui/LightModal"; +import { continueWithRootOrJailbreak } from "../../store/actions/persistedPreferences"; import { trackCieLoginSelected, trackMethodInfo, trackSpidLoginSelected } from "./analytics"; -type NavigationProps = IOStackNavigationRouteProps; - -type Props = NavigationProps & - LightModalContextInterface & - ReturnType & - ReturnType; - -type State = { - isRootedOrJailbroken: O.Option; - isSessionExpired: boolean; -}; - const getCards = ( isCIEAvailable: boolean ): ReadonlyArray> => [ @@ -179,124 +163,166 @@ export const IdpCIE: SpidIdp = { profileUrl: "" }; -class LandingScreen extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { isRootedOrJailbroken: O.none, isSessionExpired: false }; - } +export const LandingScreen = () => { + const [isRootedOrJailbroken, setIsRootedOrJailbroken] = React.useState< + O.Option + >(O.none); + const [ + hasTabletCompatibilityAlertAlreadyShown, + setHasTabletCompatibilityAlertAlreadyShown + ] = React.useState(false); + + const store = useStore(); + + const dispatch = useDispatch(); + const navigation = useNavigation(); - private isCieSupported = () => - cieFlowForDevServerEnabled || this.props.isCieSupported; - private isCieUatEnabled = () => this.props.isCieUatEnabled; + const isSessionExpired = useIOSelector(isSessionExpiredSelector); - public async componentDidMount() { + const isContinueWithRootOrJailbreak = useIOSelector( + continueWithRootOrJailbreakSelector + ); + + const isFastLoginEnabled = useIOSelector(isFastLoginEnabledSelector); + const isFastLoginOptInFFEnabled = useIOSelector(fastLoginOptInFFEnabled); + + const isCIEAuthenticationSupported = useIOSelector(isCieSupportedSelector); + const hasApiLevelSupport = useIOSelector(hasApiLevelSupportSelector); + const hasCieApiLevelSupport = pot.getOrElse(hasApiLevelSupport, false); + const hasNFCFeature = useIOSelector(hasNFCFeatureSelector); + const hasCieNFCFeature = pot.getOrElse(hasNFCFeature, false); + + const isCieSupported = React.useCallback( + () => + cieFlowForDevServerEnabled || + pot.getOrElse(isCIEAuthenticationSupported, false), + [isCIEAuthenticationSupported] + ); + const isCieUatEnabled = useIOSelector(isCieLoginUatEnabledSelector); + + useOnFirstRender(async () => { const isRootedOrJailbroken = await JailMonkey.isJailBroken(); - this.setState({ isRootedOrJailbroken: O.some(isRootedOrJailbroken) }); - if (this.props.isSessionExpired) { - this.setState({ isSessionExpired: true }); - this.props.resetState(); + setIsRootedOrJailbroken(O.some(isRootedOrJailbroken)); + if (isSessionExpired) { + dispatch(resetAuthenticationState()); } - } + }); - private displayTabletAlert() { - Alert.alert( - "", - I18n.t("tablet.message"), - [ - { - text: I18n.t("global.buttons.continue"), - style: "cancel" - } - ], - { cancelable: true } - ); - } + const { hideModal, showAnimatedModal } = React.useContext(LightModalContext); - private openUnsupportedCIEModal = () => { - this.props.showAnimatedModal( - ( - - )} - /> - ); + const displayTabletAlert = () => { + if (!hasTabletCompatibilityAlertAlreadyShown) { + setHasTabletCompatibilityAlertAlreadyShown(true); + Alert.alert( + "", + I18n.t("tablet.message"), + [ + { + text: I18n.t("global.buttons.continue"), + style: "cancel" + } + ], + { cancelable: true } + ); + } }; - private navigateToMarkdown = () => - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { - screen: ROUTES.MARKDOWN - }); + const navigateToMarkdown = React.useCallback( + () => + navigation.navigate(ROUTES.AUTHENTICATION, { + screen: ROUTES.MARKDOWN + }), + [navigation] + ); - private navigateToIdpSelection = () => { + const navigateToIdpSelection = React.useCallback(() => { trackSpidLoginSelected(); - if (this.props.isFastLoginOptInFFEnabled) { - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { + if (isFastLoginOptInFFEnabled) { + navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.AUTHENTICATION_OPT_IN, params: { identifier: "SPID" } }); } else { - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { + navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.AUTHENTICATION_IDP_SELECTION }); } - }; + }, [isFastLoginOptInFFEnabled, navigation]); + + const navigateToCiePinScreen = React.useCallback(() => { + const openUnsupportedCIEModal = () => { + showAnimatedModal( + ( + + )} + /> + ); + }; - private navigateToCiePinScreen = () => { - if (this.isCieSupported()) { - void trackCieLoginSelected(this.props.state); - this.props.dispatchIdpCieSelected(); - if (this.props.isFastLoginOptInFFEnabled) { - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { + if (isCieSupported()) { + void trackCieLoginSelected(store.getState()); + dispatch(idpSelected(IdpCIE)); + if (isFastLoginOptInFFEnabled) { + navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.AUTHENTICATION_OPT_IN, params: { identifier: "CIE" } }); } else { - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { + navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.CIE_PIN_SCREEN }); } } else { - this.openUnsupportedCIEModal(); + openUnsupportedCIEModal(); } - }; + }, [ + dispatch, + hasCieApiLevelSupport, + hasCieNFCFeature, + hideModal, + isCieSupported, + isFastLoginOptInFFEnabled, + navigation, + showAnimatedModal, + store + ]); - private navigateToSpidCieInformationRequest = () => { + const navigateToSpidCieInformationRequest = () => { trackMethodInfo(); openWebUrl(cieSpidMoreInfoUrl); }; - private navigateToCieUatSelectionScreen = () => { - if (this.isCieSupported()) { - this.props.navigation.navigate(ROUTES.AUTHENTICATION, { + const navigateToCieUatSelectionScreen = React.useCallback(() => { + if (isCieSupported()) { + navigation.navigate(ROUTES.AUTHENTICATION, { screen: ROUTES.CIE_LOGIN_CONFIG_SCREEN }); } - }; + }, [isCieSupported, navigation]); - private renderCardComponents = () => { - const cardProps = getCards(this.isCieSupported()); + const renderCardComponents = () => { + const cardProps = getCards(isCieSupported()); return cardProps.map(p => ( )); }; - private handleContinueWithRootOrJailbreak = (continueWith: boolean) => { - this.props.dispatchContinueWithRootOrJailbreak(continueWith); + const handleContinueWithRootOrJailbreak = (continueWith: boolean) => { + dispatch(continueWithRootOrJailbreak(continueWith)); }; // eslint-disable-next-line sonarjs/cognitive-complexity - private renderLandingScreen = () => { - const isCieSupported = this.isCieSupported(); - const isCieUatEnabled = this.isCieUatEnabled(); + const renderLandingScreen = () => { const firstButtonStyle = isCieUatEnabled ? styles.uatCie : styles.fullOpacity; - const secondButtonStyle = isCieSupported + const secondButtonStyle = isCieSupported() ? styles.fullOpacity : styles.noCie; return ( @@ -304,22 +330,22 @@ class LandingScreen extends React.PureComponent { appLogo contextualHelpMarkdown={contextualHelpMarkdown} faqCategories={ - isCieSupported ? ["landing_SPID", "landing_CIE"] : ["landing_SPID"] + isCieSupported() ? ["landing_SPID", "landing_CIE"] : ["landing_SPID"] } > - {isDevEnv && } + {isDevEnv && } - {this.state.isSessionExpired ? ( + {isSessionExpired ? ( ) : ( - + )} @@ -330,31 +356,32 @@ class LandingScreen extends React.PureComponent { primary={true} iconLeft={true} onPress={ - isCieSupported - ? this.navigateToCiePinScreen - : this.navigateToIdpSelection + isCieSupported() ? navigateToCiePinScreen : navigateToIdpSelection } onLongPress={() => - isCieSupported ? this.navigateToCieUatSelectionScreen() : "" + isCieSupported() ? navigateToCieUatSelectionScreen() : "" } accessibilityRole="button" accessible={true} style={firstButtonStyle} accessibilityLabel={ - isCieSupported + isCieSupported() ? I18n.t("authentication.landing.loginCie") : I18n.t("authentication.landing.loginSpid") } testID={ - isCieSupported + isCieSupported() ? "landing-button-login-cie" : "landing-button-login-spid" } > - + - {isCieSupported + {isCieSupported() ? I18n.t("authentication.landing.loginCie") : I18n.t("authentication.landing.loginSpid")} @@ -362,7 +389,7 @@ class LandingScreen extends React.PureComponent { { primary={true} iconLeft={true} onPress={ - this.isCieSupported() - ? this.navigateToIdpSelection - : this.navigateToCiePinScreen + isCieSupported() ? navigateToIdpSelection : navigateToCiePinScreen } testID={ - this.isCieSupported() + isCieSupported() ? "landing-button-login-spid" : "landing-button-login-cie" } > - {this.isCieSupported() + {isCieSupported() ? I18n.t("authentication.landing.loginSpid") : I18n.t("authentication.landing.loginCie")} @@ -397,9 +422,9 @@ class LandingScreen extends React.PureComponent { - {this.isCieSupported() + {isCieSupported() ? I18n.t("authentication.landing.nospid-nocie") : I18n.t("authentication.landing.nospid")} @@ -409,70 +434,39 @@ class LandingScreen extends React.PureComponent { }; // Screen displayed during the async loading of the JailMonkey.isJailBroken() - private renderLoadingScreen = () => ( + const renderLoadingScreen = () => ( ); - private chooseScreenToRender = (isRootedOrJailbroken: boolean) => { + const chooseScreenToRender = (isRootedOrJailbroken: boolean) => { // if the device is compromised and the user didn't allow to continue // show a blocking modal - if (isRootedOrJailbroken && !this.props.continueWithRootOrJailbreak) { + if (isRootedOrJailbroken && !isContinueWithRootOrJailbreak) { void mixpanelTrack("SHOW_ROOTED_OR_JAILBROKEN_MODAL"); return ( this.handleContinueWithRootOrJailbreak(true)} - onCancel={() => this.handleContinueWithRootOrJailbreak(false)} + onContinue={() => handleContinueWithRootOrJailbreak(true)} + onCancel={() => handleContinueWithRootOrJailbreak(false)} /> ); } // In case of Tablet, display an alert to inform the user if (DeviceInfo.isTablet()) { - this.displayTabletAlert(); + displayTabletAlert(); } // standard rendering of the landing screen - return this.renderLandingScreen(); + return renderLandingScreen(); }; - public render() { - // If the async loading of the isRootedOrJailbroken is not ready, display a loading - return pipe( - this.state.isRootedOrJailbroken, - O.fold( - () => this.renderLoadingScreen(), - // when the value isRootedOrJailbroken is ready, display the right screen based on a set of rule - rootedOrJailbroken => this.chooseScreenToRender(rootedOrJailbroken) - ) - ); - } -} - -const mapStateToProps = (state: GlobalState) => { - const isCIEAuthenticationSupported = isCieSupportedSelector(state); - const hasApiLevelSupport = hasApiLevelSupportSelector(state); - const hasNFCFeature = hasNFCFeatureSelector(state); - return { - isFastLoginEnabled: isFastLoginEnabledSelector(state), - isSessionExpired: isSessionExpiredSelector(state), - isFastLoginOptInFFEnabled: fastLoginOptInFFEnabled(state), - continueWithRootOrJailbreak: continueWithRootOrJailbreakSelector(state), - isCieSupported: pot.getOrElse(isCIEAuthenticationSupported, false), - hasCieApiLevelSupport: pot.getOrElse(hasApiLevelSupport, false), - hasCieNFCFeature: pot.getOrElse(hasNFCFeature, false), - isCieUatEnabled: isCieLoginUatEnabledSelector(state), - state - }; + // If the async loading of the isRootedOrJailbroken is not ready, display a loading + return pipe( + isRootedOrJailbroken, + O.fold( + () => renderLoadingScreen(), + // when the value isRootedOrJailbroken is ready, display the right screen based on a set of rule + rootedOrJailbroken => chooseScreenToRender(rootedOrJailbroken) + ) + ); }; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - resetState: () => dispatch(resetAuthenticationState()), - dispatchIdpCieSelected: () => dispatch(idpSelected(IdpCIE)), - dispatchContinueWithRootOrJailbreak: (continueWith: boolean) => - dispatch(continueWithRootOrJailbreak(continueWith)) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withLightModalContext(LandingScreen)); From 9634f3728e0ecea14f137e4c730af7339ed7d014 Mon Sep 17 00:00:00 2001 From: Daniel Hinterlechner <43072243+dhinterlechner@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:28:42 +0100 Subject: [PATCH 5/9] =?UTF-8?q?chore:=20Language=20=F0=9F=87=A9?= =?UTF-8?q?=F0=9F=87=AA=20adjustments=20=20(#5413)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates after new PRs and minor adjustments of too long strings --------- Co-authored-by: Jacopo Pompilii Co-authored-by: Federico Mastrini --- locales/de/index.yml | 96 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/locales/de/index.yml b/locales/de/index.yml index 04323fd863e..42f1b34816b 100644 --- a/locales/de/index.yml +++ b/locales/de/index.yml @@ -704,7 +704,7 @@ authentication: description1_2: "Alternativ kannst du die App auch über ein Login mit der" description1_3: "höchsten Sicherheitsstufe öffnen." description1_4: "Diese Art der Authentifizierung erfordert einen Benutzernamen, ein Passwort und ein physisches Medium, wie z.B. eine CIE." - title2: "Wie man den Zugang zu IO entsperrst" + title2: "Möchtest du den Zugang zu IO entsperren?" listitem1: "Stell sicher, dass du deine digitale Identität abgesichert hast, indem du deine Anmeldedaten bei deinem Identitätsanbieter änderst." listitem2_1: "Greif über das Internet auf IO zu, indem du deine SPID-Anmeldedaten oder deine CIE verwendest." listitem2_2: "Geh zur Website" @@ -1598,6 +1598,22 @@ wallet: title: "Gib die Steuernummer der Körperschaft ein" subtitle: "Sie hat 11 Ziffern und ist neben dem QR-Code zu finden." placeholder: "Steuernummer der Körperschaft" + abortDialog: + title: "Möchtest du den Vorgang abbrechen?" + confirm: "Ja, abbrechen" + cancel: "Nein, zurück" + methodSelection: + header: "Zahlungsmethode auswählen" + yourMethods: "Deine Zahlungsmethoden" + otherMethods: "Andere Zahlungsmethoden" + alert: + body: "Aufgrund des größeren Betrags sind einige Zahlungsmethoden nicht verfügbar." + cta: "OK, habe verstanden!" + missingMethodsError: + title: "Füge eine Zahlungsmethode für In-App-Zahlungen hinzu" + subtitle: "Die Zahlungsmethode wird in deinem Konto gespeichert, so dass du beim nächsten Mal einfacher bezahlen kannst." + addMethod: "Zahlungsmethode hinzufügen" + notNow: "Jetzt nicht" psp: title: "Wähle, von wem die Zahlung abgewickelt werden soll" description: "Jeder Zahlungsdienstleister (PSP) schlägt eine Provision vor." @@ -1612,6 +1628,31 @@ wallet: default: "Standard" amount: "Nach Betrag" name: "Nach Name" + failure: + PAYMENT_UNAVAILABLE: + title: "Es gibt ein technisches Problem mit dieser Zahlungsmitteilung." + PAYMENT_UNKNOWN: + title: "Die Daten der Zahlungsmitteilung sind falsch" + DOMAIN_UNKNOWN: + title: "Die ausstellende Körperschaft hat Probleme bei der Bearbeitung" + subtitle: "Bitte versuche es später noch einmal. Wenn das Problem weiterhin besteht, wende dich bitte an den Support." + PAYMENT_ONGOING: + title: "Es ist bereits eine Zahlung im Gange, bitte versuche es später noch einmal" + subtitle: "Wenn das Problem weiterhin besteht, kannst du ein Ticket erstellen." + PAYMENT_EXPIRED: + title: "Die Zahlungsmitteilung ist abgelaufen und kann nicht mehr bezahlt werden" + subtitle: "Wende dich für weitere Informationen an die Körperschaft." + PAYMENT_CANCELED: + title: "Die ausstellende Körperschaft hat diese Zahlungsmitteilung zurückgezogen" + subtitle: "Wende dich für weitere Informationen an die Körperschaft." + PAYMENT_DUPLICATED: + title: "Diese Zahlungsmitteilung wurde bereits bezahlt!" + GENERIC_ERROR: + title: "Ein unerwarteter Fehler ist aufgetreten" + subtitle: "Versuche es erneut oder wende dich an den Support." + PAA_PAGAMENTO_SCONOSCIUTO: + title: "Wir können die Zahlungsmitteilung nicht finden" + subtitle: "Die Zahlungsmitteilung kann bereits bezahlt worden sein. Wende dich bitte an den ausstellende Körperschaft." outcome: SUCCESS: title: "Du hast {{amount}} bezahlt" @@ -1619,7 +1660,44 @@ wallet: banner: title: "Kannst du uns sagen, wie es lief?" content: "Erzähl uns von deinen Erfahrungen mit der Zahlung und hilf uns, uns zu verbessern." - action: "Weiter zur Umfrage" + action: "Zur Umfrage" + GENERIC_ERROR: + title: "Es ist ein unerwarteter Fehler aufgetreten" + subtitle: "Es wurde kein Betrag abgebucht." + AUTH_ERROR: + title: "Autorisierung verweigert" + subtitle: "Es wurde kein Betrag abgebucht.\nVergewissere dich, dass du die Anweisungen deiner Bank oder Zahlungsapp korrekt befolgt hast." + INVALID_DATA: + title: "Die Daten der Zahlungsmethode sind falsch" + subtitle: "Wenn du mit einer Debit- oder Kreditkarte bezahlt hast, musst du die Daten wie auf der Karte angegeben eingeben." + TIMEOUT: + title: "Die Sitzung ist abgelaufen" + subtitle: "Es wurde kein Betrag abgebucht.\nZu deiner Sicherheit hast du nur eine begrenzte Zeit, um den Vorgang abzuschließen." + CIRCUIT_ERROR: + title: "Das Kartennetzwerk deiner Karte wird nicht unterstützt" + MISSING_FIELDS: + title: "Es fehlen einige Daten, um mit der Zahlung fortzufahren" + INVALID_CARD: + title: "Die Zahlungsmethode ist abgelaufen oder nicht mehr gültig" + subtitle: "Weitere Informationen erhältst du bei deiner Bank." + CANCELED_BY_USER: + title: "Der Vorgang wurde abgebrochen" + subtitle: "Es wurde kein Betrag abgebucht." + EXCESSIVE_AMOUNT: + title: "Autorisierung verweigert" + subtitle: "Es wurde kein Betrag abgebucht.\nDu hast wahrscheinlich die Obergrenze deiner Zahlungsmethode überschritten." + INVALID_METHOD: + title: "Die Zahlungsmethode wird nicht unterstützt" + support: + button: "Support kontaktieren" + supportTitle: "Support kontaktieren" + phone: "{{phoneNumber}} anrufen" + chat: "Im Chat um Hilfe fragen" + additionalDataTitle: "Zusätzliche Daten" + copyAll: "Alles kopieren" + errorCode: "Fehlercode" + noticeNumber: "Zahlungskodex" + entityCode: "Steuernummer Körperschaft" saveCard: saveCard: "Karte speichern" header: "Möchtest du diese Karte speichern?" @@ -2063,13 +2141,13 @@ bonus: goToDetail: "Die Karte anzeigen" detail: cta: - buyers: "CGN Rabatte und Ermäßigungen anzeigen" + buyers: "CGN Ermäßigungen anzeigen" otp: "Code generieren" eyca: copy: "EYCA Kartennummer kopieren" - pending: "EYCA Rabatte und Ermäßigungen anzeigen" + pending: "EYCA Ermäßigungen anzeigen" bottomSheet: "Besuche die EYCA Website" - showEycaDiscounts: "EYCA Rabatte und Ermäßigungen anzeigen" + showEycaDiscounts: "EYCA Ermäßigungen anzeigen" information: active: "Die Karte ist aktiv und kann bis zum {{date}} verwendet werden." warning: "Achtung! " @@ -2373,6 +2451,7 @@ whatsNew: bonusCard: validUntil: "Gültig bis {{endDate}}" expiring: "Verfällt am {{endDate}}" + expired: "Abgelaufen am {{endDate}}" paused: "In Pause" removed: "Entfernt" transaction: @@ -2381,6 +2460,7 @@ transaction: totalAmount: "Insgesamt" totalFee: "Der Gesamtbetrag umfasst " totalFeePsp: "Provision, berechnet von {{pspName}}" + totalFeeNoPsp: "Provision, die vom Transaktionsdienstleister (PSP) erhoben wird." info: title: "Informationen zur Transaktion" pspName: "Zahlungsdienstleister (PSP)" @@ -2394,7 +2474,7 @@ transaction: subject: "Zahlungsgrund" permissionRequest: gallery: - title: "Erlaube IO den Zugriff auf deine Fotos" + title: "Erlaube IO den Zugriff auf deine Fotos" subtitle: "Dazu musst du die Rechte in den Einstellungen deines Geräts ändern." caption: "So geht's:" step: "Schritt {{step}}" @@ -2405,4 +2485,8 @@ permissionRequest: 3: "Wähle “IO”" 4: "Wähle den Punkt App-Berechtigungen" 5: "Lass den Zugriff auf Dateien und Medien zu" + ios: + 1: "Geh zu “Einstellungen”" + 2: "Wähle “IO”" + 3: "Wähle “Fotos” und erlaube den Zugriff" cta: "Öffne Einstellungen" From 71d6507f19950c4bd4c05ce00ef2ccab2e664211 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:19:11 +0100 Subject: [PATCH 6/9] chore: [IOPID-1298,IOPID-1396] `Main` stack navigator - `startup` saga synch (#5420) ## Short description This PR tries to synchronize the Main stack navigator with the startup saga business logic. ## List of changes proposed in this pull request It uses the `waitForMainNavigator` generator function that acts like `waitForNavigatorServiceInitialization`. ## How to test - Run the app and during a first onboarding, before reaching the pin creation screen, re-launch the app. You'd navigate to the last screen where you was before re-launching the app. - Try different login flows, you'd be reach the messages home all the times. --- ts/navigation/AppStackNavigator.tsx | 25 ++++++++++++++--- ts/navigation/NavigationService.ts | 12 ++++++++- ts/sagas/startup.ts | 42 ++++++++++++++++++++++------- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/ts/navigation/AppStackNavigator.tsx b/ts/navigation/AppStackNavigator.tsx index b471a8cd648..35c0e5be766 100644 --- a/ts/navigation/AppStackNavigator.tsx +++ b/ts/navigation/AppStackNavigator.tsx @@ -1,5 +1,9 @@ /* eslint-disable functional/immutable-data */ -import { LinkingOptions, NavigationContainer } from "@react-navigation/native"; +import { + LinkingOptions, + NavigationContainer, + NavigationContainerProps +} from "@react-navigation/native"; import * as React from "react"; import { useRef } from "react"; import { View } from "react-native"; @@ -38,10 +42,22 @@ import { useStoredExperimentalDesign } from "../common/context/DSExperimentalCon import { IONavigationLightTheme } from "../theme/navigations"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import AuthenticatedStackNavigator from "./AuthenticatedStackNavigator"; -import NavigationService, { navigationRef } from "./NavigationService"; +import NavigationService, { + navigationRef, + setMainNavigatorReady +} from "./NavigationService"; import NotAuthenticatedStackNavigator from "./NotAuthenticatedStackNavigator"; import ROUTES from "./routes"; +type OnStateChangeStateType = Parameters< + NonNullable +>[0]; +const isMainNavigatorReady = (state: OnStateChangeStateType) => + state && + state.routes && + state.routes.length > 0 && + state.routes[0].name === ROUTES.MAIN; + export const AppStackNavigator = (): React.ReactElement => { // This hook is used since we are in a child of the Context Provider // to setup the experimental design system value from AsyncStorage @@ -153,7 +169,10 @@ const InnerNavigationContainer = (props: { children: React.ReactElement }) => { NavigationService.setNavigationReady(); routeNameRef.current = navigationRef.current?.getCurrentRoute()?.name; }} - onStateChange={async () => { + onStateChange={async state => { + if (isMainNavigatorReady(state)) { + setMainNavigatorReady(); + } const previousRouteName = routeNameRef.current; const currentRouteName = navigationRef.current?.getCurrentRoute()?.name; if (currentRouteName !== undefined) { diff --git a/ts/navigation/NavigationService.ts b/ts/navigation/NavigationService.ts index 31b2bc4d218..3679e48dd66 100644 --- a/ts/navigation/NavigationService.ts +++ b/ts/navigation/NavigationService.ts @@ -10,6 +10,13 @@ export const navigationRef = React.createRef(); // eslint-disable-next-line functional/no-let let isNavigationReady: boolean = false; +// eslint-disable-next-line functional/no-let +let isMainNavigatorReady = false; + +export const setMainNavigatorReady = () => { + isMainNavigatorReady = true; +}; + export const setNavigationReady = () => { // eslint-disable-next-line functional/immutable-data isNavigationReady = true; @@ -44,6 +51,8 @@ const dispatchNavigationAction = (action: NavigationAction) => { navigationRef.current?.dispatch(action); }; +const getIsMainNavigatorReady = () => isMainNavigatorReady; + const getCurrentRouteName = (): string | undefined => navigationRef.current?.getCurrentRoute()?.name; @@ -66,5 +75,6 @@ export default { getCurrentRoute, getCurrentState, getIsNavigationReady, - setNavigationReady + setNavigationReady, + getIsMainNavigatorReady }; diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 652e9a5e3c6..e8d0594bb0d 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -426,13 +426,11 @@ export function* initializeApplicationSaga( const watchAbortOnboardingSagaTask = yield* fork(watchAbortOnboardingSaga); yield* put(startupLoadSuccess(StartupStatusEnum.ONBOARDING)); - // FIXME IOPID-1298: find any better way to handle this - // We need this workaround to let the inner AppStackNavigator stack be ready, - // before continuing with any other navigation action to avoid: - // Error: The 'navigation' object hasn't been initialized yet... - // Here the navigationRef is ready, but because we changed the navigation inner stack - // based on StartupStatusEnum value, we need to wait for the new stack to be ready. - yield* delay(0 as Millisecond); + if (!handleSessionExpiration) { + yield* call(waitForMainNavigator); + } + + // yield* delay(0 as Millisecond); const hasPreviousSessionAndPin = previousSessionToken && O.isSome(maybeStoredPin); if (hasPreviousSessionAndPin && showIdentificationModal) { @@ -517,9 +515,6 @@ export function* initializeApplicationSaga( yield* call(updateInstallationSaga, backendClient.createOrUpdateInstallation); yield* put(startupLoadSuccess(StartupStatusEnum.AUTHENTICATED)); - // FIXME IOPID-1298: find any better way to handle this - // As above for StartupStatusEnum.ONBOARDING - yield* delay(0 as Millisecond); // // User is autenticated, session token is valid // @@ -695,6 +690,33 @@ function* waitForNavigatorServiceInitialization() { }); } +function* waitForMainNavigator() { + // eslint-disable-next-line functional/no-let + let isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady); + + // eslint-disable-next-line functional/no-let + let timeoutLogged = false; + const startTime = performance.now(); + + // before continuing we must wait for the main navigator tack to be ready + while (!isMainNavReady) { + const elapsedTime = performance.now() - startTime; + if (!timeoutLogged && elapsedTime >= warningWaitNavigatorTime) { + timeoutLogged = true; + + yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_TIMEOUT"); + } + yield* delay(navigatorPollingTime); + isMainNavReady = yield* call(NavigationService.getIsMainNavigatorReady); + } + + const initTime = performance.now() - startTime; + + yield* call(mixpanelTrack, "MAIN_NAVIGATOR_STACK_READY_OK", { + elapsedTime: initTime + }); +} + /** * Remove all the local notifications related to authentication with spid. * From 893d6e4ffeef697316c8811fbca3382a1e7b4223 Mon Sep 17 00:00:00 2001 From: Alessandro Izzo Date: Wed, 24 Jan 2024 09:55:16 +0100 Subject: [PATCH 7/9] fix: [IOBP-512] Wallet details with new generic error screen (#5427) ## Short description This PR implements the new generic error screen using the `OperationResultScreenContent` component. ## List of changes proposed in this pull request - Removed from `WalletDetailsScreen` the old and deprecated `WorkunitGenericFailure` with the new one ## How to test Start and complete an onboarding flow with a new card and when you are in the method details screen, try to force an error on that page. ## Preview |Before|After| |-|-| | | | Co-authored-by: Federico Mastrini --- locales/en/index.yml | 5 +++ locales/it/index.yml | 5 +++ .../details/screens/WalletDetailsScreen.tsx | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index 43350d48fbb..e0e59a0642b 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -1013,6 +1013,11 @@ wallet: title: Vuoi pagare in app? content: Scopri quali sono i metodi che puoi aggiungere al tuo portafoglio. cta: Scopri di più + error: + title: Si è verificato un errore + subtitle: Riprova o contatta l'assistenza + primaryButton: Chiudi + secondaryButton: Riprova wallet: Wallet refreshWallet: Refresh the Wallet favourite: diff --git a/locales/it/index.yml b/locales/it/index.yml index 87e81440b7b..f5a8d5b56b3 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -1013,6 +1013,11 @@ wallet: title: Vuoi pagare in app? content: Scopri quali sono i metodi che puoi aggiungere al tuo portafoglio. cta: Scopri di più + error: + title: Si è verificato un errore + subtitle: Riprova o contatta l'assistenza + primaryButton: Chiudi + secondaryButton: Riprova wallet: Portafoglio refreshWallet: Aggiorna il Portafoglio favourite: diff --git a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx index 2eec49f09b4..d371306e44b 100644 --- a/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx +++ b/ts/features/walletV3/details/screens/WalletDetailsScreen.tsx @@ -1,12 +1,11 @@ import * as React from "react"; -import { RouteProp, useRoute } from "@react-navigation/native"; +import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; import { useDispatch } from "react-redux"; import { IOLogoPaymentExtType } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; -import WorkunitGenericFailure from "../../../../components/error/WorkunitGenericFailure"; import { PaymentCardBig } from "../../../../components/ui/cards/payment/PaymentCardBig"; import { useIOSelector } from "../../../../store/hooks"; import { idPayAreInitiativesFromInstrumentLoadingSelector } from "../../../idpay/wallet/store/reducers"; @@ -22,6 +21,12 @@ import { import { walletDetailsGetInstrument } from "../store/actions"; import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; import { getDateFromExpiryDate } from "../../../../utils/dates"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; +import I18n from "../../../../i18n"; +import { + AppParamsList, + IOStackNavigationProp +} from "../../../../navigation/params/AppParamsList"; export type WalletDetailsScreenNavigationParams = Readonly<{ walletId: string; @@ -71,6 +76,7 @@ const generateCardHeaderTitle = (details?: UIWalletInfoDetails) => { */ const WalletDetailsScreen = () => { const route = useRoute(); + const navigation = useNavigation>(); const dispatch = useDispatch(); const { walletId } = route.params; const walletDetails = useIOSelector(walletDetailsInstrumentSelector); @@ -82,6 +88,30 @@ const WalletDetailsScreen = () => { idPayAreInitiativesFromInstrumentLoadingSelector ); + const WalletDetailsGenericFailure = () => ( + navigation.pop() + }} + secondaryAction={{ + label: I18n.t("wallet.methodDetails.error.secondaryButton"), + accessibilityLabel: I18n.t( + "wallet.methodDetails.error.secondaryButton" + ), + onPress: handleOnRetry + }} + /> + ); + + const handleOnRetry = () => { + dispatch(walletDetailsGetInstrument.request({ walletId })); + }; + React.useEffect(() => { dispatch(walletDetailsGetInstrument.request({ walletId })); }, [walletId, dispatch]); @@ -122,7 +152,7 @@ const WalletDetailsScreen = () => { ); } else if (isErrorWalletDetails) { - return ; + return ; } return null; }; From 3e0f184820d5a0667984207f53863b5cfb044c07 Mon Sep 17 00:00:00 2001 From: Alessandro Izzo Date: Wed, 24 Jan 2024 10:11:50 +0100 Subject: [PATCH 8/9] chore: [IOBP-316] Add return to origin page after payment flow completion (#5399) ## Short description This PR implements a new feature that allows users to automatically navigate back to their origin page once they have completed (successfully or not) the payment process. This enhancement improves user experience by providing a seamless transition back to their initial context after finishing a transaction. ## List of changes proposed in this pull request - Added an additional argument into `walletPaymentInitState` action that expects an optional route name to go back to after the payment is completed/canceled. ## How to test - Start a new payment flow until you reach the end of it; - When you close the outcome message, you should be able to go back to the origin point that started the flow ## Preview https://github.com/pagopa/io-app/assets/34343582/c4a42ab0-8676-4a7a-8631-624c3a98191a --------- Co-authored-by: Federico Mastrini --- .../screens/WalletPaymentOutcomeScreen.tsx | 12 ++++++++- .../payment/store/actions/orchestration.ts | 4 +++ .../walletV3/payment/store/reducers/index.ts | 25 ++++++++++++++++++- .../walletV3/payment/store/selectors/index.ts | 5 ++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx index ae321c1e63e..6654b6bb063 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentOutcomeScreen.tsx @@ -17,7 +17,10 @@ import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { WalletPaymentFeebackBanner } from "../components/WalletPaymentFeedbackBanner"; import { usePaymentFailureSupportModal } from "../hooks/usePaymentFailureSupportModal"; import { WalletPaymentParamsList } from "../navigation/params"; -import { walletPaymentDetailsSelector } from "../store/selectors"; +import { + walletPaymentDetailsSelector, + walletPaymentStartRouteSelector +} from "../store/selectors"; import { WalletPaymentOutcome, WalletPaymentOutcomeEnum @@ -38,6 +41,7 @@ const WalletPaymentOutcomeScreen = () => { const navigation = useNavigation>(); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); + const paymentStartRoute = useIOSelector(walletPaymentStartRouteSelector); const supportModal = usePaymentFailureSupportModal({ outcome @@ -55,6 +59,12 @@ const WalletPaymentOutcomeScreen = () => { }; const handleClose = () => { + if (paymentStartRoute) { + navigation.navigate(paymentStartRoute.routeName, { + screen: paymentStartRoute.routeKey + }); + return; + } navigation.popToTop(); navigation.pop(); }; diff --git a/ts/features/walletV3/payment/store/actions/orchestration.ts b/ts/features/walletV3/payment/store/actions/orchestration.ts index c306089e00a..cba31c50fa9 100644 --- a/ts/features/walletV3/payment/store/actions/orchestration.ts +++ b/ts/features/walletV3/payment/store/actions/orchestration.ts @@ -2,6 +2,10 @@ import { ActionType, createStandardAction } from "typesafe-actions"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; +/** + * Action to initialize the state of a payment, optionally you can specify the route to go back to + * after the payment is completed or cancelled (default is the popToTop route) + */ export const walletPaymentInitState = createStandardAction( "WALLET_PAYMENT_INIT_STATE" )(); diff --git a/ts/features/walletV3/payment/store/reducers/index.ts b/ts/features/walletV3/payment/store/reducers/index.ts index d96dd69e5c9..036bb117b78 100644 --- a/ts/features/walletV3/payment/store/reducers/index.ts +++ b/ts/features/walletV3/payment/store/reducers/index.ts @@ -1,7 +1,10 @@ import _ from "lodash"; import * as pot from "@pagopa/ts-commons/lib/pot"; import * as O from "fp-ts/lib/Option"; +import { NavigatorScreenParams } from "@react-navigation/native"; import { getType } from "typesafe-actions"; +import { pipe } from "fp-ts/lib/function"; +import { sequenceS } from "fp-ts/lib/Apply"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; import { NewTransactionResponse } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; @@ -27,6 +30,8 @@ import { import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; import { WalletPaymentFailure } from "../../types/failure"; import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import NavigationService from "../../../../../navigation/NavigationService"; +import { AppParamsList } from "../../../../../navigation/params/AppParamsList"; export type WalletPaymentState = { rptId?: RptId; @@ -44,6 +49,10 @@ export type WalletPaymentState = { NetworkError | WalletPaymentFailure >; authorizationUrl: pot.Pot; + startRoute?: { + routeName: keyof AppParamsList; + routeKey: keyof NavigatorScreenParams["screen"]; + }; }; const INITIAL_STATE: WalletPaymentState = { @@ -64,7 +73,21 @@ const reducer = ( ): WalletPaymentState => { switch (action.type) { case getType(walletPaymentInitState): - return INITIAL_STATE; + const startRoute = pipe( + sequenceS(O.Monad)({ + routeName: O.fromNullable( + NavigationService.getCurrentRouteName() as keyof AppParamsList + ), + routeKey: O.fromNullable( + NavigationService.getCurrentRouteKey() as keyof NavigatorScreenParams["screen"] + ) + }), + O.toUndefined + ); + return { + ...INITIAL_STATE, + startRoute + }; // Payment verification and details case getType(walletPaymentGetDetails.request): diff --git a/ts/features/walletV3/payment/store/selectors/index.ts b/ts/features/walletV3/payment/store/selectors/index.ts index 20afcaaf1b8..e122cb38c2f 100644 --- a/ts/features/walletV3/payment/store/selectors/index.ts +++ b/ts/features/walletV3/payment/store/selectors/index.ts @@ -76,3 +76,8 @@ export const walletPaymentAuthorizationUrlSelector = createSelector( selectWalletPayment, state => state.authorizationUrl ); + +export const walletPaymentStartRouteSelector = createSelector( + selectWalletPayment, + state => state.startRoute +); From d34be635f123b5141b88fb2df2c4963811af3ead Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 24 Jan 2024 11:07:34 +0100 Subject: [PATCH 9/9] chore: fix E2E tests after main stack navigator and startup saga sync (#5429) This PR depends on #5420 ## Short description This PR fixes the E2E tests, following changes from the stack navigator and startup saga sync. ## List of changes proposed in this pull request - reloadReactNative replaced with launchApp({ newInstance: true}) to prevent bad behaviours - improved a credit card test by searching for a testId instead of a text - better system keyboard closing ## How to test Run the E2E tests. --------- Co-authored-by: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> --- ts/__e2e__/payment.e2e.ts | 7 ++++--- ts/__e2e__/utils.ts | 10 ++++++++++ ts/components/wallet/WalletHomeHeader.tsx | 1 + ts/features/bonus/cgn/__e2e__/cgn.e2e.ts | 2 +- .../bonus/cgn/components/detail/CgnUnsubscribe.tsx | 7 +------ .../bonus/cgn/components/detail/ToastPatch.e2e.ts | 1 - ts/features/bonus/cgn/components/detail/ToastPatch.ts | 1 - .../euCovidCert/__e2e__/euCovidCertExpired.e2e.ts | 2 +- .../euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts | 2 +- .../euCovidCert/__e2e__/euCovidCertValid.e2e.ts | 2 +- ts/features/messages/__e2e__/messages.e2e.ts | 2 +- .../onboarding/__e2e__/creditCardOnboarding.e2e.ts | 10 +++++----- 12 files changed, 26 insertions(+), 21 deletions(-) delete mode 100644 ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts delete mode 100644 ts/features/bonus/cgn/components/detail/ToastPatch.ts diff --git a/ts/__e2e__/payment.e2e.ts b/ts/__e2e__/payment.e2e.ts index 2586a887670..53b6f2553ba 100644 --- a/ts/__e2e__/payment.e2e.ts +++ b/ts/__e2e__/payment.e2e.ts @@ -1,11 +1,11 @@ import I18n from "../i18n"; import { formatNumberCentsToAmount } from "../utils/stringBuilder"; import { e2eWaitRenderTimeout } from "./config"; -import { ensureLoggedIn } from "./utils"; +import { closeKeyboard, ensureLoggedIn } from "./utils"; describe("Payment", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); @@ -106,8 +106,9 @@ describe("Payment", () => { await element(matchNoticeCodeInput).typeText("123123123123123123"); await element(by.id("EntityCodeInputMask")).typeText("12345678901"); + // Close the keyboard - await element(by.label("Fine")).atIndex(0).tap(); + await closeKeyboard(); await element(by.text(I18n.t("global.buttons.continue"))).tap(); diff --git a/ts/__e2e__/utils.ts b/ts/__e2e__/utils.ts index eeca5bb9847..ef0e68658fb 100644 --- a/ts/__e2e__/utils.ts +++ b/ts/__e2e__/utils.ts @@ -117,3 +117,13 @@ export const ensureLoggedIn = async () => { await loginWithSPID(); } }; + +export const closeKeyboard = async () => { + // Sometimes the device ignores the locale set by the detox setup + // In such case we can try to close the keyboard using the english translation + try { + await element(by.label("Fine")).atIndex(0).tap(); + } catch (e) { + await element(by.label("Done")).atIndex(0).tap(); + } +}; diff --git a/ts/components/wallet/WalletHomeHeader.tsx b/ts/components/wallet/WalletHomeHeader.tsx index f1e9c364215..4098a42100c 100644 --- a/ts/components/wallet/WalletHomeHeader.tsx +++ b/ts/components/wallet/WalletHomeHeader.tsx @@ -48,6 +48,7 @@ export const useWalletHomeHeaderBottomSheet = (): IOBottomSheetModal => { const navigationListItems: ReadonlyArray = [ { title: I18n.t("wallet.paymentMethod"), + testId: "wallet.paymentMethod", subtitle: I18n.t("wallet.paymentMethodDesc"), onPress: () => navigateToWalletAddPaymentMethod({ diff --git a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts b/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts index d92b2e4167a..37d9defeadf 100644 --- a/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts +++ b/ts/features/bonus/cgn/__e2e__/cgn.e2e.ts @@ -42,7 +42,7 @@ const activateBonusSuccess = async () => { describe("CGN", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx index 9ef0c1fd4cd..ec0eebcd348 100644 --- a/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx +++ b/ts/features/bonus/cgn/components/detail/CgnUnsubscribe.tsx @@ -11,7 +11,6 @@ import { isError, isReady } from "../../../../../common/model/RemoteValue"; import { navigateBack } from "../../../../../store/actions/navigation"; import { cgnDetails } from "../../store/actions/details"; import { IOToast } from "../../../../../components/Toast"; -import { skipToastShowingDueToE2ECrash } from "./ToastPatch"; const CgnUnsubscribe = () => { const dispatch = useIODispatch(); @@ -39,11 +38,7 @@ const CgnUnsubscribe = () => { if (isReady(unsubscriptionStatus)) { navigateBack(); dispatch(cgnDetails.request()); - // This is needed to prevent a crash while running E2E tests. Showing - // the toast causes random crashes upon calling device.reloadReactNative - if (!skipToastShowingDueToE2ECrash) { - IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); - } + IOToast.success(I18n.t("bonus.cgn.activation.deactivate.toast")); } if (isError(unsubscriptionStatus) && !isFirstRender.current) { IOToast.error(I18n.t("global.genericError")); diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts deleted file mode 100644 index cd3658899d7..00000000000 --- a/ts/features/bonus/cgn/components/detail/ToastPatch.e2e.ts +++ /dev/null @@ -1 +0,0 @@ -export const skipToastShowingDueToE2ECrash = true; diff --git a/ts/features/bonus/cgn/components/detail/ToastPatch.ts b/ts/features/bonus/cgn/components/detail/ToastPatch.ts deleted file mode 100644 index b6d723d80f9..00000000000 --- a/ts/features/bonus/cgn/components/detail/ToastPatch.ts +++ /dev/null @@ -1 +0,0 @@ -export const skipToastShowingDueToE2ECrash = false; diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts index 9237521d1ad..84b19fee096 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertExpired.e2e.ts @@ -11,7 +11,7 @@ const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; describe("EuCovidCert Expired", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts index 4424b3d2c17..c2de8638b12 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertRevoked.e2e.ts @@ -11,7 +11,7 @@ const learnMoreLinkTestId = "euCovidCertLearnMoreLink"; describe("EuCovidCert Revoked", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts b/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts index 2782e192109..8f2fe2ac19b 100644 --- a/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts +++ b/ts/features/euCovidCert/__e2e__/euCovidCertValid.e2e.ts @@ -13,7 +13,7 @@ const fullScreenQrCodeTestId = "fullScreenQRCode"; describe("EuCovidCert Valid", () => { beforeAll(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/messages/__e2e__/messages.e2e.ts b/ts/features/messages/__e2e__/messages.e2e.ts index 28a723b3d65..a9d2037e1b3 100644 --- a/ts/features/messages/__e2e__/messages.e2e.ts +++ b/ts/features/messages/__e2e__/messages.e2e.ts @@ -4,7 +4,7 @@ import { ensureLoggedIn } from "../../../__e2e__/utils"; describe("Messages Screen", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); diff --git a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts index 642a158fd29..195c5d6cffa 100644 --- a/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts +++ b/ts/features/wallet/onboarding/__e2e__/creditCardOnboarding.e2e.ts @@ -1,10 +1,10 @@ import { e2eWaitRenderTimeout } from "../../../../__e2e__/config"; -import { ensureLoggedIn } from "../../../../__e2e__/utils"; +import { closeKeyboard, ensureLoggedIn } from "../../../../__e2e__/utils"; import I18n from "../../../../i18n"; describe("Credit Card onboarding", () => { beforeEach(async () => { - await device.reloadReactNative(); + await device.launchApp({ newInstance: true }); await ensureLoggedIn(); }); @@ -24,12 +24,12 @@ describe("Credit Card onboarding", () => { // Button "+ Add" await element(by.id("walletAddNewPaymentMethodTestId")).tap(); - await waitFor(element(by.text(I18n.t("wallet.paymentMethod")))) + await waitFor(element(by.id("wallet.paymentMethod"))) .toBeVisible() .withTimeout(e2eWaitRenderTimeout); // Add payment method listItem in bottomSheet - await element(by.text(I18n.t("wallet.paymentMethod"))).tap(); + await element(by.id("wallet.paymentMethod")).tap(); await waitFor(element(by.text(I18n.t("wallet.methods.card.name")))) .toBeVisible() @@ -51,7 +51,7 @@ describe("Credit Card onboarding", () => { await element(by.id("securityCodeInputMask")).typeText("123"); // Close the keyboard - await element(by.label("Fine")).atIndex(0).tap(); + await closeKeyboard(); await element(by.text(I18n.t("global.buttons.continue"))).tap(); await waitFor(element(by.id("saveOrContinueButton")))