Skip to content

Commit

Permalink
feat(Bonus Pagamenti Digitali): [#176455937] Error and loading screen…
Browse files Browse the repository at this point in the history
… transaction details (#2755)

* [#176455937] remove unused load prop

* [#176455937] refresh transactions list on go to transaction button press

* [#176455937] renamed BpdTransactionsScreen in BpdAvailableTransactionsScreen

* [#176455937] refactor BpdTransactionsScreen as proxy screen

* [#176455937] add LoadTransactions component

* [#176455937] move BpdAvailableTransactionsScreen in detail dir

* [#176455937] add loading and error labels

* [#176455937] move screen file outside details dir

* [#176455937] add transactions unavailable screen

* [#176455937] refactor BpdTransactionsScreen taking into account lastUpdate status

* [#176455937] fix lint error loadingScreen

* [#176455937] move transactions reload inside BpdTransactionsScreen

* [#176455937] remove reactotron

* [#176455937] add test files

* [#176455937] add testId to the image

* [#176455937] add TransactionsUnavailable tests

* [#176455937] add testId

* [#176455937] refactor TransactionsUnavailable tests

* [#176455937] LoadTransactions tests

* [#176455937] add testId to identify different screen

* [#176455937] add bpdTransactionsScreen tests

* [#176455937] add some transactionsScreen tests

* [#176455937] refactor tests

* [#176455937] refactor BpdTransactionsScreen

* [#176455937] fix test

* [#176455937] remove async on test

* [#176455937] remove useless brackets

* [#176455937] remove old comment

* [#176455937] fix comment typo

* [#176455937] remove callback chain

Co-authored-by: fabriziofff <[email protected]>
  • Loading branch information
debiff and fabriziofff authored Feb 10, 2021
1 parent fe4d3b0 commit ae071e4
Show file tree
Hide file tree
Showing 12 changed files with 1,102 additions and 289 deletions.
5 changes: 4 additions & 1 deletion locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
4 changes: 4 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
14 changes: 12 additions & 2 deletions ts/components/infoScreen/InfoScreenComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export const InfoScreenStyle = styles;

const renderNode = (body: string | React.ReactNode) => {
if (typeof body === "string") {
return <Text style={styles.body}>{body}</Text>;
return (
<Text style={styles.body} testID={"infoScreenBody"}>
{body}
</Text>
);
}

return body;
Expand All @@ -53,7 +57,13 @@ export const InfoScreenComponent: React.FunctionComponent<Props> = props => {
<NavigationEvents onDidFocus={() => setAccessibilityFocus(elementRef)} />
{props.image}
<View spacer={true} large={true} />
<Text style={styles.title} bold={true} ref={elementRef} accessible={true}>
<Text
style={styles.title}
bold={true}
ref={elementRef}
accessible={true}
testID={"infoScreenTitle"}
>
{props.title}
</Text>
<View spacer={true} />
Expand Down
7 changes: 6 additions & 1 deletion ts/components/infoScreen/imageRendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ const styles = StyleSheet.create({
* @param image
*/
export const renderInfoRasterImage = (image: ImageSourcePropType) => (
<Image source={image} resizeMode={"contain"} style={styles.raster} />
<Image
source={image}
resizeMode={"contain"}
style={styles.raster}
testID={"rasterImage"}
/>
);

export const renderInfoIconImage = (
Expand Down
1 change: 0 additions & 1 deletion ts/features/bonus/bpd/screens/details/BpdDetailsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ const BpdDetailsScreen: React.FunctionComponent<Props> = props => {
};

const mapDispatchToProps = (dispatch: Dispatch) => ({
load: () => dispatch(bpdAllData.request()),
completeUnsubscription: () => {
dispatch(bpdAllData.request());
dispatch(bpdUnsubscribeCompleted());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof mapDispatchToProps> &
ReturnType<typeof mapStateToProps>;

type TotalCashbackPerDate = {
trxDate: Date;
totalCashBack: number;
};

const dataForFlatList = (
transactions: pot.Pot<ReadonlyArray<EnhancedBpdTransaction>, 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<EnhancedBpdTransaction>,
cashbackAward: number
): ReadonlyArray<
SectionListData<EnhancedBpdTransaction | TotalCashbackPerDate>
> => {
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<EnhancedBpdTransaction | TotalCashbackPerDate>;
}): React.ReactNode => (
<BaseDailyTransactionHeader
date={info.section.title}
transactionsNumber={
[...info.section.data].filter(i => !isTotalCashback(i)).length
}
/>
);

export const NoPaymentMethodAreActiveWarning = () => (
<View>
<InfoBox iconName={"io-warning"}>
<H4 weight={"Regular"}>
{I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text1")}
<H4 weight={"Bold"}>
{I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text2")}
</H4>
{I18n.t("bonus.bpd.details.transaction.noPaymentMethod.text3")}
</H4>
</InfoBox>
<View spacer={true} small={true} />
</View>
);

/**
* Display all the transactions for a specific period
* TODO: scroll to refresh, display error, display loading
* @constructor
*/
const BpdAvailableTransactionsScreen: React.FunctionComponent<Props> = 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 (
<BpdCashbackMilestoneComponent
cashbackValue={fromNullable(props.selectedPeriod).fold(
0,
p => p.maxPeriodCashback
)}
/>
);
}
return <BpdTransactionItem transaction={info.item} />;
};

return (
<BaseScreenComponent
goBack={true}
headerTitle={I18n.t("bonus.bpd.title")}
contextualHelp={emptyContextualHelp}
>
<SafeAreaView
style={IOStyles.flex}
testID={"BpdAvailableTransactionsScreen"}
>
<View spacer={true} />
<View style={IOStyles.horizontalContentPadding}>
<H1>{I18n.t("bonus.bpd.details.transaction.title")}</H1>
</View>
<ScrollView style={[IOStyles.flex]}>
<View style={IOStyles.horizontalContentPadding}>
<View spacer={true} />
{props.selectedPeriod && maybeLastUpdateDate.isSome() && (
<>
<BpdTransactionSummaryComponent
lastUpdateDate={localeDateFormat(
maybeLastUpdateDate.value,
I18n.t("global.dateFormats.fullFormatFullMonthLiteral")
)}
period={props.selectedPeriod}
totalAmount={props.selectedPeriod.amount}
/>
<View spacer={true} />
</>
)}
</View>
{props.selectedPeriod &&
(transactions.length > 0 ? (
<SectionList
renderSectionHeader={renderSectionHeader}
scrollEnabled={true}
stickySectionHeadersEnabled={true}
sections={getTransactionsByDaySections(
trxSortByDate,
props.selectedPeriod.maxPeriodCashback
)}
renderItem={renderTransactionItem}
keyExtractor={t =>
isTotalCashback(t)
? `awarded_cashback_item${t.totalCashBack}`
: t.keyId
}
/>
) : !props.atLeastOnePaymentMethodActive &&
pot.isSome(props.potWallets) &&
props.potWallets.value.length > 0 ? (
<View style={IOStyles.horizontalContentPadding}>
<NoPaymentMethodAreActiveWarning />
</View>
) : (
<View style={IOStyles.horizontalContentPadding}>
<BpdEmptyTransactionsList />
</View>
))}
</ScrollView>
</SafeAreaView>
</BaseScreenComponent>
);
};

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);
Loading

0 comments on commit ae071e4

Please sign in to comment.