diff --git a/locales/en/index.yml b/locales/en/index.yml index 64537c9e6ea..11f8768ca6f 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -2047,13 +2047,16 @@ bonus: body: "The ranking is based on the **number of transactions** made with the payment methods on which you have Cashback enabled.\n\n We will show your ranking **when the distribution of participants will be more meaningful**. Currently, many users collected the same number of transactions.\n\n Once published, the ranking will **continue to change** depending on the number of transactions among all participants, until the end of the 6 months." - transaction: label: one: "1 transaction" other: "{{transactions}} transactions" title: "Your transactions" goToButton: "Transactions detail" + loading: "We're retrieving your transactions\n\nPlease hold on..." + error: + title: "Systems are taking longer than expected" + body: "We're currently unable to retrieve your transactions. Don't worry! The list will be available again soon." noPaymentMethod: text1: "You must activate " text2: "at least one payment method " diff --git a/locales/it/index.yml b/locales/it/index.yml index 87a4f8100d7..8e8d78298f0 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -2085,6 +2085,10 @@ bonus: other: "{{transactions}} transazioni" title: "Le tue transazioni" goToButton: "Dettaglio transazioni" + loading: "Stiamo recuperando la lista delle tue transazioni\n\nAttendi qualche secondo..." + error: + title: "I sistemi stanno impiegando più tempo del previsto" + body: "In questo momento non riusciamo a recuperare le tue transazioni. Non preoccuparti, a breve l’elenco tornerà nuovamente disponibile." noPaymentMethod: text1: "Devi attivare " text2: "almeno un metodo di pagamento " diff --git a/ts/components/infoScreen/InfoScreenComponent.tsx b/ts/components/infoScreen/InfoScreenComponent.tsx index b729b73fedf..b0b8fa6cd9d 100644 --- a/ts/components/infoScreen/InfoScreenComponent.tsx +++ b/ts/components/infoScreen/InfoScreenComponent.tsx @@ -35,7 +35,11 @@ export const InfoScreenStyle = styles; const renderNode = (body: string | React.ReactNode) => { if (typeof body === "string") { - return {body}; + return ( + + {body} + + ); } return body; @@ -53,7 +57,13 @@ export const InfoScreenComponent: React.FunctionComponent = props => { setAccessibilityFocus(elementRef)} /> {props.image} - + {props.title} diff --git a/ts/components/infoScreen/imageRendering.tsx b/ts/components/infoScreen/imageRendering.tsx index e3e12096b15..bd4df6e97af 100644 --- a/ts/components/infoScreen/imageRendering.tsx +++ b/ts/components/infoScreen/imageRendering.tsx @@ -33,7 +33,12 @@ const styles = StyleSheet.create({ * @param image */ export const renderInfoRasterImage = (image: ImageSourcePropType) => ( - + ); export const renderInfoIconImage = ( diff --git a/ts/features/bonus/bpd/screens/details/BpdDetailsScreen.tsx b/ts/features/bonus/bpd/screens/details/BpdDetailsScreen.tsx index ac04dcf582c..fc99635c794 100644 --- a/ts/features/bonus/bpd/screens/details/BpdDetailsScreen.tsx +++ b/ts/features/bonus/bpd/screens/details/BpdDetailsScreen.tsx @@ -121,7 +121,6 @@ const BpdDetailsScreen: React.FunctionComponent = props => { }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - load: () => dispatch(bpdAllData.request()), completeUnsubscription: () => { dispatch(bpdAllData.request()); dispatch(bpdUnsubscribeCompleted()); diff --git a/ts/features/bonus/bpd/screens/details/transaction/BpdAvailableTransactionsScreen.tsx b/ts/features/bonus/bpd/screens/details/transaction/BpdAvailableTransactionsScreen.tsx new file mode 100644 index 00000000000..ed0901ae6d9 --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/BpdAvailableTransactionsScreen.tsx @@ -0,0 +1,316 @@ +import { compareDesc } from "date-fns"; +import { index, reverse } from "fp-ts/lib/Array"; +import { fromNullable } from "fp-ts/lib/Option"; +import * as pot from "italia-ts-commons/lib/pot"; +import { View } from "native-base"; +import * as React from "react"; +import { + SafeAreaView, + ScrollView, + SectionList, + SectionListData, + SectionListRenderItem +} from "react-native"; +import { connect } from "react-redux"; +import { Dispatch } from "redux"; +import { InfoBox } from "../../../../../../components/box/InfoBox"; +import { H1 } from "../../../../../../components/core/typography/H1"; +import { H4 } from "../../../../../../components/core/typography/H4"; +import { IOStyles } from "../../../../../../components/core/variables/IOStyles"; +import BaseScreenComponent from "../../../../../../components/screens/BaseScreenComponent"; +import I18n from "../../../../../../i18n"; +import { GlobalState } from "../../../../../../store/reducers/types"; +import { emptyContextualHelp } from "../../../../../../utils/emptyContextualHelp"; +import { localeDateFormat } from "../../../../../../utils/locale"; +import BaseDailyTransactionHeader from "../../../components/BaseDailyTransactionHeader"; +import BpdTransactionSummaryComponent from "../../../components/BpdTransactionSummaryComponent"; +import { + BpdTransactionItem, + EnhancedBpdTransaction +} from "../../../components/transactionItem/BpdTransactionItem"; +import { + atLeastOnePaymentMethodHasBpdEnabledSelector, + bpdDisplayTransactionsSelector, + paymentMethodsWithActivationStatusSelector +} from "../../../store/reducers/details/combiner"; +import { bpdSelectedPeriodSelector } from "../../../store/reducers/details/selectedPeriod"; +import BpdCashbackMilestoneComponent from "./BpdCashbackMilestoneComponent"; +import BpdEmptyTransactionsList from "./BpdEmptyTransactionsList"; + +export type Props = ReturnType & + ReturnType; + +type TotalCashbackPerDate = { + trxDate: Date; + totalCashBack: number; +}; + +const dataForFlatList = ( + transactions: pot.Pot, Error> +) => pot.getOrElse(transactions, []); + +export const isTotalCashback = (item: any): item is TotalCashbackPerDate => + item.totalCashBack !== undefined; + +/** + * Builds the array of objects needed to show the sectionsList grouped by transaction day. + * + * We check the subtotal of TotalCashback earned on each transaction to check when the user reaches the cashback. + * + * When creating the final array if we reached the cashback amount we set all the following transaction cashback value to 0 + * + * If the sum of cashback comes over the award we remove the exceeding part on the transaction. + * @param transactions + * @param cashbackAward + */ +const getTransactionsByDaySections = ( + transactions: ReadonlyArray, + cashbackAward: number +): ReadonlyArray< + SectionListData +> => { + const dates = [ + ...new Set( + transactions.map(trx => + localeDateFormat(trx.trxDate, I18n.t("global.dateFormats.dayFullMonth")) + ) + ) + ]; + + const transactionsAsc = reverse([...transactions]); + + // accumulator to define when the user reached the cashback award amount + // and tracing the sum of all the cashback value to check if any negative trx may cause a revoke of cashback award + const amountWinnerAccumulator = transactionsAsc.reduce( + ( + acc: { + winner?: { + amount: number; + index: number; + date: Date; + }; + sumAmount: number; + }, + t: EnhancedBpdTransaction, + currIndex: number + ) => { + const sum = acc.sumAmount + t.cashback; + if (sum >= cashbackAward && !acc.winner) { + return { + winner: { + amount: sum, + index: currIndex, + date: new Date(t.trxDate) + }, + sumAmount: sum + }; + } else if (sum < cashbackAward) { + return { + sumAmount: sum + }; + } + return { + ...acc, + sumAmount: sum + }; + }, + { + sumAmount: 0 + } + ); + + const maybeWinner = fromNullable(amountWinnerAccumulator.winner); + + // If the user reached the cashback amount within transactions we actualize all the cashback value starting from the index of winning transaction + // if the winning transaction makes cashback value exceed the limit we set the amount to the difference of transaction cashback value, total amout at winnign transaction and cashback award limit. + // all the following transactions will be set to 0 cashback value, since the limit has been reached (a dedicated item will be displayed) + const updatedTransactions = [...transactionsAsc].map((t, i) => { + if (maybeWinner.isSome()) { + if ( + i === maybeWinner.value.index && + maybeWinner.value.amount > cashbackAward + ) { + return { + ...t, + cashback: t.cashback - (maybeWinner.value.amount - cashbackAward) + }; + } else if (i > maybeWinner.value.index) { + return { + ...t, + cashback: 0 + }; + } + } + return t; + }); + + return dates.map(d => ({ + title: d, + data: [ + ...updatedTransactions.filter( + t => + localeDateFormat( + t.trxDate, + I18n.t("global.dateFormats.dayFullMonth") + ) === d + ), + // we add the the data array an item to display the milestone reached + // in order to display the milestone after the latest transaction summed in the total we add 1 ms so that the ordering will set it correctly + ...maybeWinner.fold([], w => { + if ( + localeDateFormat( + w.date, + I18n.t("global.dateFormats.dayFullMonth") + ) === d + ) { + return [ + { + totalCashBack: w.amount, + trxDate: new Date( + w.date.setMilliseconds(w.date.getMilliseconds() + 1) + ) + } + ]; + } + return []; + }) + ].sort((trx1, trx2) => compareDesc(trx1.trxDate, trx2.trxDate)) + })); +}; + +const renderSectionHeader = (info: { + section: SectionListData; +}): React.ReactNode => ( + !isTotalCashback(i)).length + } + /> +); + +export const NoPaymentMethodAreActiveWarning = () => ( + + +

+ {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text1")} +

+ {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text2")} +

+ {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text3")} + +
+ + +); + +/** + * Display all the transactions for a specific period + * TODO: scroll to refresh, display error, display loading + * @constructor + */ +const BpdAvailableTransactionsScreen: React.FunctionComponent = props => { + const transactions = dataForFlatList(props.transactionForSelectedPeriod); + + const trxSortByDate = [...transactions].sort((trx1, trx2) => + compareDesc(trx1.trxDate, trx2.trxDate) + ); + + const maybeLastUpdateDate = index(0, [...trxSortByDate]).map(t => t.trxDate); + + const renderTransactionItem: SectionListRenderItem< + EnhancedBpdTransaction | TotalCashbackPerDate + > = info => { + if (isTotalCashback(info.item)) { + return ( + p.maxPeriodCashback + )} + /> + ); + } + return ; + }; + + return ( + + + + +

{I18n.t("bonus.bpd.details.transaction.title")}

+
+ + + + {props.selectedPeriod && maybeLastUpdateDate.isSome() && ( + <> + + + + )} + + {props.selectedPeriod && + (transactions.length > 0 ? ( + + isTotalCashback(t) + ? `awarded_cashback_item${t.totalCashBack}` + : t.keyId + } + /> + ) : !props.atLeastOnePaymentMethodActive && + pot.isSome(props.potWallets) && + props.potWallets.value.length > 0 ? ( + + + + ) : ( + + + + ))} + +
+
+ ); +}; + +const mapDispatchToProps = (_: Dispatch) => ({}); + +const mapStateToProps = (state: GlobalState) => ({ + transactionForSelectedPeriod: bpdDisplayTransactionsSelector(state), + selectedPeriod: bpdSelectedPeriodSelector(state), + potWallets: paymentMethodsWithActivationStatusSelector(state), + atLeastOnePaymentMethodActive: atLeastOnePaymentMethodHasBpdEnabledSelector( + state + ) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BpdAvailableTransactionsScreen); diff --git a/ts/features/bonus/bpd/screens/details/transaction/BpdTransactionsScreen.tsx b/ts/features/bonus/bpd/screens/details/transaction/BpdTransactionsScreen.tsx index f487c3a1d88..0253aa6ec0c 100644 --- a/ts/features/bonus/bpd/screens/details/transaction/BpdTransactionsScreen.tsx +++ b/ts/features/bonus/bpd/screens/details/transaction/BpdTransactionsScreen.tsx @@ -1,310 +1,71 @@ -import { compareDesc } from "date-fns"; -import { index, reverse } from "fp-ts/lib/Array"; -import { fromNullable } from "fp-ts/lib/Option"; import * as pot from "italia-ts-commons/lib/pot"; -import { View } from "native-base"; import * as React from "react"; -import { - SafeAreaView, - ScrollView, - SectionList, - SectionListData, - SectionListRenderItem -} from "react-native"; import { connect } from "react-redux"; import { Dispatch } from "redux"; -import { InfoBox } from "../../../../../../components/box/InfoBox"; -import { H1 } from "../../../../../../components/core/typography/H1"; -import { H4 } from "../../../../../../components/core/typography/H4"; -import { IOStyles } from "../../../../../../components/core/variables/IOStyles"; -import BaseScreenComponent from "../../../../../../components/screens/BaseScreenComponent"; -import I18n from "../../../../../../i18n"; import { GlobalState } from "../../../../../../store/reducers/types"; -import { emptyContextualHelp } from "../../../../../../utils/emptyContextualHelp"; -import { localeDateFormat } from "../../../../../../utils/locale"; -import BaseDailyTransactionHeader from "../../../components/BaseDailyTransactionHeader"; -import BpdTransactionSummaryComponent from "../../../components/BpdTransactionSummaryComponent"; -import { - BpdTransactionItem, - EnhancedBpdTransaction -} from "../../../components/transactionItem/BpdTransactionItem"; -import { - atLeastOnePaymentMethodHasBpdEnabledSelector, - bpdDisplayTransactionsSelector, - paymentMethodsWithActivationStatusSelector -} from "../../../store/reducers/details/combiner"; -import { bpdSelectedPeriodSelector } from "../../../store/reducers/details/selectedPeriod"; -import BpdCashbackMilestoneComponent from "./BpdCashbackMilestoneComponent"; -import BpdEmptyTransactionsList from "./BpdEmptyTransactionsList"; +import { EnhancedBpdTransaction } from "../../../components/transactionItem/BpdTransactionItem"; +import { bpdAllData } from "../../../store/actions/details"; +import { bpdDisplayTransactionsSelector } from "../../../store/reducers/details/combiner"; +import { bpdLastUpdateSelector } from "../../../store/reducers/details/lastUpdate"; +import BpdAvailableTransactionsScreen from "./BpdAvailableTransactionsScreen"; +import LoadTransactions from "./LoadTransactions"; +import TransactionsUnavailable from "./TransactionsUnavailable"; export type Props = ReturnType & ReturnType; -type TotalCashbackPerDate = { - trxDate: Date; - totalCashBack: number; -}; - -const dataForFlatList = ( - transactions: pot.Pot, Error> -) => pot.getOrElse(transactions, []); - -export const isTotalCashback = (item: any): item is TotalCashbackPerDate => - item.totalCashBack !== undefined; - /** - * Builds the array of objects needed to show the sectionsList grouped by transaction day. - * - * We check the subtotal of TotalCashback earned on each transaction to check when the user reaches the cashback. - * - * When creating the final array if we reached the cashback amount we set all the following transaction cashback value to 0 - * - * If the sum of cashback comes over the award we remove the exceeding part on the transaction. + * Associate at every state of the pot transactions status the right screen to show * @param transactions - * @param cashbackAward */ -const getTransactionsByDaySections = ( - transactions: ReadonlyArray, - cashbackAward: number -): ReadonlyArray< - SectionListData -> => { - const dates = [ - ...new Set( - transactions.map(trx => - localeDateFormat(trx.trxDate, I18n.t("global.dateFormats.dayFullMonth")) - ) - ) - ]; - - const transactionsAsc = reverse([...transactions]); - - // accumulator to define when the user reached the cashback award amount - // and tracing the sum of all the cashback value to check if any negative trx may cause a revoke of cashback award - const amountWinnerAccumulator = transactionsAsc.reduce( - ( - acc: { - winner?: { - amount: number; - index: number; - date: Date; - }; - sumAmount: number; - }, - t: EnhancedBpdTransaction, - currIndex: number - ) => { - const sum = acc.sumAmount + t.cashback; - if (sum >= cashbackAward && !acc.winner) { - return { - winner: { - amount: sum, - index: currIndex, - date: new Date(t.trxDate) - }, - sumAmount: sum - }; - } else if (sum < cashbackAward) { - return { - sumAmount: sum - }; - } - return { - ...acc, - sumAmount: sum - }; - }, - { - sumAmount: 0 - } +const handleTransactionsStatus = ( + transactions: pot.Pot, Error> +) => + pot.fold( + transactions, + () => , + () => , + _ => , + _ => , + _ => , + _ => , + _ => , + _ => ); - const maybeWinner = fromNullable(amountWinnerAccumulator.winner); - - // If the user reached the cashback amount within transactions we actualize all the cashback value starting from the index of winning transaction - // if the the winning transaction makes cashback value exceed the limit we set the amount to the difference of transaction cashback value, total amout at winnign transaction and cashback award limit. - // all the following transactions will be set to 0 cashback value, since the limit has been reached (a dedicated item will be displayed) - const updatedTransactions = [...transactionsAsc].map((t, i) => { - if (maybeWinner.isSome()) { - if ( - i === maybeWinner.value.index && - maybeWinner.value.amount > cashbackAward - ) { - return { - ...t, - cashback: t.cashback - (maybeWinner.value.amount - cashbackAward) - }; - } else if (i > maybeWinner.value.index) { - return { - ...t, - cashback: 0 - }; - } - } - return t; - }); - - return dates.map(d => ({ - title: d, - data: [ - ...updatedTransactions.filter( - t => - localeDateFormat( - t.trxDate, - I18n.t("global.dateFormats.dayFullMonth") - ) === d - ), - // we add the the data array an item to display the milestone reached - // in order to display the milestone after the latest transaction summed in the total we add 1 ms so that the ordering will set it correctly - ...maybeWinner.fold([], w => { - if ( - localeDateFormat( - w.date, - I18n.t("global.dateFormats.dayFullMonth") - ) === d - ) { - return [ - { - totalCashBack: w.amount, - trxDate: new Date( - w.date.setMilliseconds(w.date.getMilliseconds() + 1) - ) - } - ]; - } - return []; - }) - ].sort((trx1, trx2) => compareDesc(trx1.trxDate, trx2.trxDate)) - })); -}; - -const renderSectionHeader = (info: { - section: SectionListData; -}): React.ReactNode => ( - !isTotalCashback(i)).length - } - /> -); - -export const NoPaymentMethodAreActiveWarning = () => ( - - -

- {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text1")} -

- {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text2")} -

- {I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text3")} - -
- - -); - /** - * Display all the transactions for a specific period - * TODO: scroll to refresh, display error, display loading + * Display all the transactions for a specific period if available, in other case show a loading or an error screen. + * First check the whole bpd status than if is some check the transactions status. * @constructor */ -const BpdTransactionsScreen: React.FunctionComponent = props => { - const transactions = dataForFlatList(props.transactionForSelectedPeriod); - - const trxSortByDate = [...transactions].sort((trx1, trx2) => - compareDesc(trx1.trxDate, trx2.trxDate) - ); - - const maybeLastUpdateDate = index(0, [...trxSortByDate]).map(t => t.trxDate); - - const renderTransactionItem: SectionListRenderItem< - EnhancedBpdTransaction | TotalCashbackPerDate - > = info => { - if (isTotalCashback(info.item)) { - return ( - p.maxPeriodCashback - )} - /> - ); +const BpdTransactionsScreen: React.FC = (props: Props) => { + React.useEffect(() => { + if ( + pot.isError(props.bpdLastUpdate) || + pot.isError(props.transactionForSelectedPeriod) + ) { + props.loadTransactions(); } - return ; - }; - - return ( - - - - -

{I18n.t("bonus.bpd.details.transaction.title")}

-
- - - - {props.selectedPeriod && maybeLastUpdateDate.isSome() && ( - <> - - - - )} - - {props.selectedPeriod && - (transactions.length > 0 ? ( - - isTotalCashback(t) - ? `awarded_cashback_item${t.totalCashBack}` - : t.keyId - } - /> - ) : !props.atLeastOnePaymentMethodActive && - pot.isSome(props.potWallets) && - props.potWallets.value.length > 0 ? ( - - - - ) : ( - - - - ))} - -
-
+ }, []); + return pot.fold( + props.bpdLastUpdate, + () => , + () => , + _ => , + _ => , + _ => handleTransactionsStatus(props.transactionForSelectedPeriod), + _ => , + _ => , + _ => ); }; - -const mapDispatchToProps = (_: Dispatch) => ({}); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loadTransactions: () => dispatch(bpdAllData.request()) +}); const mapStateToProps = (state: GlobalState) => ({ transactionForSelectedPeriod: bpdDisplayTransactionsSelector(state), - selectedPeriod: bpdSelectedPeriodSelector(state), - potWallets: paymentMethodsWithActivationStatusSelector(state), - atLeastOnePaymentMethodActive: atLeastOnePaymentMethodHasBpdEnabledSelector( - state - ) + bpdLastUpdate: bpdLastUpdateSelector(state) }); export default connect( diff --git a/ts/features/bonus/bpd/screens/details/transaction/LoadTransactions.tsx b/ts/features/bonus/bpd/screens/details/transaction/LoadTransactions.tsx new file mode 100644 index 00000000000..30eee57e5e0 --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/LoadTransactions.tsx @@ -0,0 +1,29 @@ +import { View } from "native-base"; +import * as React from "react"; +import { ActivityIndicator } from "react-native"; +import { IOStyles } from "../../../../../../components/core/variables/IOStyles"; +import { InfoScreenComponent } from "../../../../../../components/infoScreen/InfoScreenComponent"; +import I18n from "../../../../../../i18n"; + +/** + * This screen is displayed when loading the list of transactions + * @constructor + */ +const LoadTransactions: React.FunctionComponent = () => ( + + + } + title={I18n.t("bonus.bpd.details.transaction.loading")} + /> + +); + +export default LoadTransactions; diff --git a/ts/features/bonus/bpd/screens/details/transaction/TransactionsUnavailable.tsx b/ts/features/bonus/bpd/screens/details/transaction/TransactionsUnavailable.tsx new file mode 100644 index 00000000000..19e18989f5f --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/TransactionsUnavailable.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { SafeAreaView } from "react-native"; +import image from "../../../../../../../img/wallet/errors/payment-unavailable-icon.png"; +import { IOStyles } from "../../../../../../components/core/variables/IOStyles"; +import { renderInfoRasterImage } from "../../../../../../components/infoScreen/imageRendering"; +import { InfoScreenComponent } from "../../../../../../components/infoScreen/InfoScreenComponent"; +import BaseScreenComponent from "../../../../../../components/screens/BaseScreenComponent"; +import I18n from "../../../../../../i18n"; + +export type Props = Pick< + React.ComponentProps, + "contextualHelp" +>; + +const loadLocales = () => ({ + headerTitle: I18n.t("bonus.bpd.details.transaction.goToButton"), + title: I18n.t("bonus.bpd.details.transaction.error.title"), + body: I18n.t("bonus.bpd.details.transaction.error.body") +}); + +/** + * This screen informs the user that there are problems retrieving the transactions list. + * @constructor + */ +const TransactionsUnavailable: React.FunctionComponent = props => { + const { headerTitle, title, body } = loadLocales(); + + return ( + + + + + + ); +}; + +export default TransactionsUnavailable; diff --git a/ts/features/bonus/bpd/screens/details/transaction/__test__/BpdTransactionsScreen.test.tsx b/ts/features/bonus/bpd/screens/details/transaction/__test__/BpdTransactionsScreen.test.tsx new file mode 100644 index 00000000000..be2b1e054dc --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/__test__/BpdTransactionsScreen.test.tsx @@ -0,0 +1,530 @@ +import * as React from "react"; +import { Action, createStore } from "redux"; +import { NavigationParams } from "react-navigation"; +import { View } from "native-base"; +import * as pot from "italia-ts-commons/lib/pot"; +import BpdTransactionsScreen from "../BpdTransactionsScreen"; +import { appReducer } from "../../../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../../../store/actions/application"; +import { renderScreenFakeNavRedux } from "../../../../../../../utils/testWrapper"; +import { GlobalState } from "../../../../../../../store/reducers/types"; +import ROUTES from "../../../../../../../navigation/routes"; +import { reproduceSequence } from "../../../../../../../utils/tests"; +import { bpdAllData } from "../../../../store/actions/details"; +import * as lastUpdateReducer from "../../../../store/reducers/details/lastUpdate"; +import * as transactionsReducer from "../../../../store/reducers/details/combiner"; +import { + BpdTransaction, + BpdTransactions, + bpdTransactionsLoad +} from "../../../../store/actions/transactions"; +import { AwardPeriodId } from "../../../../store/actions/periods"; +import { navigateToBpdDetails } from "../../../../navigation/actions"; +import { BpdPeriodWithInfo } from "../../../../store/reducers/details/periods"; +import * as BpdTransactionItem from "../../../../components/transactionItem/BpdTransactionItem"; +import * as BpdTransactionSummaryComponent from "../../../../components/BpdTransactionSummaryComponent"; + +// Be sure that navigation is unmocked +jest.unmock("react-navigation"); +jest.mock("react-native-share", () => ({ + open: jest.fn() +})); +describe("BpdTransactionsScreen", () => { + beforeEach(() => jest.useFakeTimers()); + it("should show the TransactionUnavailable screen if bpdLastUpdate is pot.none", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, globalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("TransactionUnavailable")).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.noneLoading", () => { + const sequenceOfActions: ReadonlyArray = [bpdAllData.request()]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.noneUpdating", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const finalState: GlobalState = { + ...globalState, + bonus: { + ...globalState.bonus, + bpd: { + ...globalState.bonus.bpd, + details: { + ...globalState.bonus.bpd.details, + lastUpdate: pot.noneUpdating({} as Date) + } + } + } + }; + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the TransactionUnavailable screen if bpdLastUpdate is pot.noneError", () => { + const myspy = jest + .spyOn(lastUpdateReducer, "bpdLastUpdateSelector") + .mockReturnValue(pot.noneError({} as Error)); + + const sequenceOfActions: ReadonlyArray = [ + bpdAllData.request(), + bpdAllData.failure({ message: "error" } as Error) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("TransactionUnavailable")).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + + myspy.mockRestore(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.someLoading", () => { + const sequenceOfActions: ReadonlyArray = [ + bpdAllData.request(), + bpdAllData.success(), + bpdAllData.request() + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the TransactionUnavailable screen if bpdLastUpdate is pot.someError", () => { + const myspy = jest + .spyOn(lastUpdateReducer, "bpdLastUpdateSelector") + .mockReturnValue(pot.someError({} as Date, {} as Error)); + + const sequenceOfActions: ReadonlyArray = [ + bpdAllData.request(), + bpdAllData.success(), + bpdAllData.request() + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("TransactionUnavailable")).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + myspy.mockRestore(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.someUpdating", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const finalState: GlobalState = { + ...globalState, + bonus: { + ...globalState.bonus, + bpd: { + ...globalState.bonus.bpd, + details: { + ...globalState.bonus.bpd.details, + lastUpdate: pot.someUpdating({} as Date, {} as Date) + } + } + } + }; + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the BpdAvailableTransactionsScreen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.none", () => { + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success() + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.noneLoading", () => { + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success(), + bpdTransactionsLoad.request(1 as AwardPeriodId) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.noneUpdating", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const finalState: GlobalState = { + ...globalState, + bonus: { + ...globalState.bonus, + bpd: { + ...globalState.bonus.bpd, + details: { + ...globalState.bonus.bpd.details, + lastUpdate: pot.some({} as Date), + selectedPeriod: { + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo, + transactions: { + 1: pot.noneUpdating([]) + } + } + } + } + }; + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the TransactionUnavailable screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.noneError", () => { + const lastUpdateSpy = jest + .spyOn(lastUpdateReducer, "bpdLastUpdateSelector") + .mockReturnValue(pot.some({} as Date)); + const transactionsSpy = jest + .spyOn(transactionsReducer, "bpdDisplayTransactionsSelector") + .mockReturnValue(pot.noneError({} as Error)); + + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success(), + bpdTransactionsLoad.request(1 as AwardPeriodId), + bpdTransactionsLoad.failure({ + awardPeriodId: 1 as AwardPeriodId, + error: {} as Error + }) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("TransactionUnavailable")).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + lastUpdateSpy.mockRestore(); + transactionsSpy.mockRestore(); + }); + it("should show the BpdAvailableTransactionsScreen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.some", () => { + jest + .spyOn(BpdTransactionItem, "BpdTransactionItem") + .mockReturnValue(); + jest + .spyOn(BpdTransactionSummaryComponent, "default") + .mockReturnValue(); + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success(), + bpdTransactionsLoad.request(1 as AwardPeriodId), + bpdTransactionsLoad.success({ + awardPeriodId: 1 as AwardPeriodId, + results: [ + { + amount: 0.7114042004081611, + awardPeriodId: 1 as AwardPeriodId, + cashback: 0.5640133257839899, + circuitType: "Unknown", + hashPan: "hashPan", + idTrxAcquirer: "idTrxAcquirer", + idTrxIssuer: "idTrxIssuer", + trxDate: new Date() + } as BpdTransaction + ] + } as BpdTransactions) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.someLoading", () => { + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success(), + bpdTransactionsLoad.request(1 as AwardPeriodId), + bpdTransactionsLoad.success({ + awardPeriodId: 1 as AwardPeriodId, + results: [ + { + amount: 0.7114042004081611, + awardPeriodId: 1 as AwardPeriodId, + cashback: 0.5640133257839899, + circuitType: "Unknown", + hashPan: "hashPan", + idTrxAcquirer: "idTrxAcquirer", + idTrxIssuer: "idTrxIssuer", + trxDate: new Date() + } as BpdTransaction + ] + } as BpdTransactions), + bpdTransactionsLoad.request(1 as AwardPeriodId) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the LoadTransactions screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.someUpdating", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const finalState: GlobalState = { + ...globalState, + bonus: { + ...globalState.bonus, + bpd: { + ...globalState.bonus.bpd, + details: { + ...globalState.bonus.bpd.details, + lastUpdate: pot.some({} as Date), + selectedPeriod: { + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo, + transactions: { + 1: pot.someUpdating([], []) + } + } + } + } + }; + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + expect(component).toBeDefined(); + expect(component.queryByTestId("LoadTransactions")).toBeTruthy(); + expect(component.queryByTestId("TransactionUnavailable")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + }); + it("should show the TransactionUnavailable screen if bpdLastUpdate is pot.some and transactionForSelectedPeriod is pot.someError", () => { + const lastUpdateSpy = jest + .spyOn(lastUpdateReducer, "bpdLastUpdateSelector") + .mockReturnValue(pot.some({} as Date)); + const transactionsSpy = jest + .spyOn(transactionsReducer, "bpdDisplayTransactionsSelector") + .mockReturnValue(pot.someError([], {} as Error)); + + const sequenceOfActions: ReadonlyArray = [ + navigateToBpdDetails({ + awardPeriodId: 1 as AwardPeriodId + } as BpdPeriodWithInfo), + bpdAllData.request(), + bpdAllData.success(), + bpdTransactionsLoad.request(1 as AwardPeriodId), + bpdTransactionsLoad.success({} as BpdTransactions), + bpdTransactionsLoad.request(1 as AwardPeriodId), + bpdTransactionsLoad.failure({ + awardPeriodId: 1 as AwardPeriodId, + error: {} as Error + }) + ]; + + const finalState = reproduceSequence( + {} as GlobalState, + appReducer, + sequenceOfActions + ); + const component = renderScreenFakeNavRedux( + () => , + ROUTES.WALLET_BPAY_DETAIL, + {}, + createStore(appReducer, finalState as any) + ); + + // expect(finalState.bonus.bpd.details.transactions).toBe(""); + expect(component).toBeDefined(); + expect(component.queryByTestId("TransactionUnavailable")).toBeTruthy(); + expect(component.queryByTestId("LoadTransactions")).toBeNull(); + expect( + component.queryByTestId("BpdAvailableTransactionsScreen") + ).toBeNull(); + transactionsSpy.mockRestore(); + lastUpdateSpy.mockRestore(); + }); +}); diff --git a/ts/features/bonus/bpd/screens/details/transaction/__test__/LoadTransactions.test.tsx b/ts/features/bonus/bpd/screens/details/transaction/__test__/LoadTransactions.test.tsx new file mode 100644 index 00000000000..65af8ec8008 --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/__test__/LoadTransactions.test.tsx @@ -0,0 +1,38 @@ +import { render } from "@testing-library/react-native"; +import * as React from "react"; +import I18n from "../../../../../../../i18n"; +import LoadTransactions from "../LoadTransactions"; + +jest.mock("react-navigation", () => ({ + NavigationEvents: "mockNavigationEvents", + StackActions: { + push: jest + .fn() + .mockImplementation(x => ({ ...x, type: "Navigation/PUSH" })), + replace: jest + .fn() + .mockImplementation(x => ({ ...x, type: "Navigation/REPLACE" })), + reset: jest.fn() + }, + NavigationActions: { + navigate: jest.fn().mockImplementation(x => x) + }, + createStackNavigator: jest.fn(), + withNavigation: (component: any) => component +})); +describe("LoadTransactions component", () => { + it("should show the activity indicator", () => { + const component = render(); + const activityIndicator = component.queryByTestId("activityIndicator"); + + expect(activityIndicator).not.toBe(null); + }); + it("should show the right title", () => { + const component = render(); + const title = component.getByText( + I18n.t("bonus.bpd.details.transaction.loading") + ); + + expect(title).not.toBeEmpty(); + }); +}); diff --git a/ts/features/bonus/bpd/screens/details/transaction/__test__/TransactionsUnavailable.test.tsx b/ts/features/bonus/bpd/screens/details/transaction/__test__/TransactionsUnavailable.test.tsx new file mode 100644 index 00000000000..bc818b4e5eb --- /dev/null +++ b/ts/features/bonus/bpd/screens/details/transaction/__test__/TransactionsUnavailable.test.tsx @@ -0,0 +1,73 @@ +import { render } from "@testing-library/react-native"; +import configureMockStore from "redux-mock-store"; +import * as React from "react"; +import { Provider } from "react-redux"; +import { Store } from "redux"; +import I18n from "../../../../../../../i18n"; +import TransactionsUnavailable from "../TransactionsUnavailable"; + +jest.mock("react-navigation", () => ({ + NavigationEvents: "mockNavigationEvents", + StackActions: { + push: jest + .fn() + .mockImplementation(x => ({ ...x, type: "Navigation/PUSH" })), + replace: jest + .fn() + .mockImplementation(x => ({ ...x, type: "Navigation/REPLACE" })), + reset: jest.fn() + }, + NavigationActions: { + navigate: jest.fn().mockImplementation(x => x) + }, + createStackNavigator: jest.fn(), + withNavigation: (component: any) => component +})); +describe("TransactionsUnavailable component", () => { + const mockStore = configureMockStore(); + // eslint-disable-next-line functional/no-let + let store: ReturnType; + + beforeEach(() => { + jest.useFakeTimers(); + store = mockStore({ + search: { isSearchEnabled: false }, + persistedPreferences: { isPagoPATestEnabled: false }, + network: { isConnected: true }, + instabug: { unreadMessages: 0 } + }); + }); + + it("should show the payment-unavailable-icon.png", () => { + const component = getComponent(store); + const rasterImageComponent = component.queryByTestId("rasterImage"); + const paymentUnavailableIconPath = + "../../../img/wallet/errors/payment-unavailable-icon.png"; + + expect(rasterImageComponent).toHaveProp("source", { + testUri: paymentUnavailableIconPath + }); + }); + it("should use the right string as header, title and body", () => { + const component = getComponent(store); + const headerTitle = component.getByText( + I18n.t("bonus.bpd.details.transaction.goToButton") + ); + const title = component.getByText( + I18n.t("bonus.bpd.details.transaction.error.title") + ); + const body = component.getByText( + I18n.t("bonus.bpd.details.transaction.error.body") + ); + + expect(headerTitle).not.toBeEmpty(); + expect(title).not.toBeEmpty(); + expect(body).not.toBeEmpty(); + }); +}); +const getComponent = (store: Store) => + render( + + + + );