ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx
import {
ButtonSolid,
Chip,
ContentWrapper,
Pictogram,
Tag,
VSpacer
} from "@pagopa/io-app-design-system";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { useNavigation, useRoute } from "@react-navigation/core";
import { RouteProp, useFocusEffect } from "@react-navigation/native";
import { sequenceS } from "fp-ts/lib/Apply";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import * as React from "react";
import { StyleSheet, View } from "react-native";
import Animated, { Layout } from "react-native-reanimated";
import {
InitiativeDTO,
InitiativeRewardTypeEnum,
StatusEnum
} from "../../../../../definitions/idpay/InitiativeDTO";
import { BonusCardScreenComponent } from "../../../../components/BonusCard";
import { BonusCardCounter } from "../../../../components/BonusCard/BonusCardCounter";
import { BonusStatus } from "../../../../components/BonusCard/type";
import { Body } from "../../../../components/core/typography/Body";
import { H3 } from "../../../../components/core/typography/H3";
import { IOScrollViewActions } from "../../../../components/ui/IOScrollView";
import I18n from "../../../../i18n";
import {
AppParamsList,
IOStackNavigationProp
} from "../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { format } from "../../../../utils/dates";
import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder";
import { IdPayCodeCieBanner } from "../../code/components/IdPayCodeCieBanner";
import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes";
import { IdPayInitiativeLastUpdateCounter } from "../components/IdPayInitiativeLastUpdateCounter";
import { InitiativeDiscountSettingsComponent } from "../components/InitiativeDiscountSettingsComponent";
import { InitiativeRefundSettingsComponent } from "../components/InitiativeRefundSettingsComponent";
import {
InitiativeTimelineComponent,
InitiativeTimelineComponentSkeleton
} from "../components/InitiativeTimelineComponent";
import { MissingConfigurationAlert } from "../components/MissingConfigurationAlert";
import { useIdPayDiscountDetailsBottomSheet } from "../hooks/useIdPayDiscountDetailsBottomSheet";
import { IDPayDetailsParamsList, IDPayDetailsRoutes } from "../navigation";
import {
idpayInitiativeDetailsSelector,
initiativeNeedsConfigurationSelector
} from "../store";
import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions";
import { ConfigurationMode } from "../../configuration/types";
export type IdPayInitiativeDetailsScreenParams = {
initiativeId: string;
};
type IdPayInitiativeDetailsScreenRouteProps = RouteProp<
IDPayDetailsParamsList,
"IDPAY_DETAILS_MONITORING"
>;
const IdPayInitiativeDetailsScreen = () => {
const route = useRoute<IdPayInitiativeDetailsScreenRouteProps>();
const { initiativeId } = route.params;
const navigation = useNavigation<IOStackNavigationProp<AppParamsList>>();
const dispatch = useIODispatch();
const initiativeDataPot = useIOSelector(idpayInitiativeDetailsSelector);
const navigateToBeneficiaryDetails = () => {
navigation.push(IDPayDetailsRoutes.IDPAY_DETAILS_MAIN, {
screen: IDPayDetailsRoutes.IDPAY_DETAILS_BENEFICIARY,
params: {
initiativeId,
initiativeName: pot.getOrElse(
pot.map(initiativeDataPot, initiative => initiative.initiativeName),
undefined
)
}
});
};
const navigateToConfiguration = () => {
navigation.push(IdPayConfigurationRoutes.IDPAY_CONFIGURATION_NAVIGATOR, {
screen: IdPayConfigurationRoutes.IDPAY_CONFIGURATION_INTRO,
params: { initiativeId, mode: ConfigurationMode.COMPLETE }
});
};
const discountBottomSheet = useIdPayDiscountDetailsBottomSheet(initiativeId);
useFocusEffect(
React.useCallback(() => {
dispatch(idpayInitiativeGet.request({ initiativeId }));
dispatch(
idpayTimelinePageGet.request({ initiativeId, page: 0, pageSize: 5 })
);
}, [dispatch, initiativeId])
);
const initiativeNeedsConfiguration = useIOSelector(
initiativeNeedsConfigurationSelector
);
if (!pot.isSome(initiativeDataPot)) {
return (
<BonusCardScreenComponent isLoading={true}>
<ContentWrapper>
<VSpacer size={8} />
<IdPayInitiativeLastUpdateCounter isLoading={true} />
<VSpacer size={8} />
<InitiativeTimelineComponentSkeleton size={5} />
<VSpacer size={32} />
</ContentWrapper>
</BonusCardScreenComponent>
);
}
const getInitiativeCounters = (
initiative: InitiativeDTO
): ReadonlyArray<BonusCardCounter> => {
const availableAmount = initiative.amountCents || 0;
const accruedAmount = initiative.accruedCents || 0;
const amountProgress = pipe(
sequenceS(O.Monad)({
amount: O.fromNullable(initiative.amountCents),
accrued: O.fromNullable(initiative.accruedCents),
refunded: O.fromNullable(initiative.refundedCents)
}),
O.map(({ amount, accrued, refunded }) => ({
total: amount + accrued + refunded,
amount
})),
O.filter(({ total }) => total !== 0),
O.map(({ amount, total }) => (amount / total) * 100.0),
O.getOrElse(() => 100.0)
);
return pipe(
initiative.initiativeRewardType,
O.fromNullable,
O.alt(() => O.some(InitiativeRewardTypeEnum.REFUND)),
O.fold(
() => [],
(type): ReadonlyArray<BonusCardCounter> => {
switch (type) {
case InitiativeRewardTypeEnum.DISCOUNT:
return [
{
type: "ValueWithProgress",
label: I18n.t(
"idpay.initiative.details.initiativeCard.availableAmount"
),
value: formatNumberCentsToAmount(
availableAmount,
true,
"right"
),
progress: amountProgress
}
];
case InitiativeRewardTypeEnum.REFUND:
return [
{
type: "ValueWithProgress",
label: I18n.t(
"idpay.initiative.details.initiativeCard.availableAmount"
),
value: formatNumberCentsToAmount(
availableAmount,
true,
"right"
),
progress: amountProgress
},
{
type: "Value",
label: I18n.t(
"idpay.initiative.details.initiativeCard.toRefund"
),
value: formatNumberCentsToAmount(accruedAmount, true, "right")
}
];
}
}
)
);
};
const getInitiativeDetailsContent = (initiative: InitiativeDTO) =>
pipe(
initiative.initiativeRewardType,
O.fromNullable,
O.alt(() => O.some(InitiativeRewardTypeEnum.REFUND)),
O.fold(
() => undefined,
rewardType => {
switch (rewardType) {
case InitiativeRewardTypeEnum.DISCOUNT:
return (
<ContentWrapper>
<VSpacer size={8} />
<IdPayCodeCieBanner initiativeId={initiative.initiativeId} />
<Animated.View layout={Layout.duration(200)}>
<InitiativeTimelineComponent
initiativeId={initiative.initiativeId}
size={5}
/>
<VSpacer size={32} />
<InitiativeDiscountSettingsComponent
initiative={initiative}
/>
<VSpacer size={16} />
</Animated.View>
</ContentWrapper>
);
case InitiativeRewardTypeEnum.REFUND:
if (initiativeNeedsConfiguration) {
return (
<View style={styles.newInitiativeMessageContainer}>
<Pictogram name="empty" size={72} />
<VSpacer size={16} />
<H3>
{I18n.t(
"idpay.initiative.details.initiativeDetailsScreen.notConfigured.header"
)}
</H3>
<VSpacer size={8} />
<Body style={{ textAlign: "center" }}>
{I18n.t(
"idpay.initiative.details.initiativeDetailsScreen.notConfigured.footer",
{ initiative: initiative.initiativeName }
)}
</Body>
<VSpacer size={16} />
<ButtonSolid
accessibilityLabel={I18n.t(
"idpay.initiative.details.initiativeDetailsScreen.configured.startConfigurationCTA"
)}
fullWidth={true}
color="primary"
onPress={navigateToConfiguration}
label={I18n.t(
"idpay.initiative.details.initiativeDetailsScreen.configured.startConfigurationCTA"
)}
/>
</View>
);
}
return (
<ContentWrapper>
<MissingConfigurationAlert
initiativeId={initiativeId}
status={initiative.status}
/>
<VSpacer size={8} />
<InitiativeTimelineComponent
initiativeId={initiativeId}
size={3}
/>
<VSpacer size={24} />
<InitiativeRefundSettingsComponent initiative={initiative} />
<VSpacer size={32} />
</ContentWrapper>
);
}
}
)
);
const getInitiativeFooterProps = (
rewardType?: InitiativeRewardTypeEnum
): IOScrollViewActions | undefined => {
switch (rewardType) {
case InitiativeRewardTypeEnum.DISCOUNT:
return {
type: "SingleButton",
primary: {
label: I18n.t("idpay.initiative.discountDetails.authorizeButton"),
onPress: discountBottomSheet.present
}
};
default:
case InitiativeRewardTypeEnum.REFUND:
return undefined;
}
};
const initiative = initiativeDataPot.value;
const {
initiativeName,
organizationName,
lastCounterUpdate,
initiativeRewardType,
logoURL
} = initiative;
return (
<BonusCardScreenComponent
headerAction={{
icon: "info",
onPress: navigateToBeneficiaryDetails,
accessibilityLabel: "info"
}}
logoUris={[{ uri: logoURL }]}
name={initiativeName || ""}
organizationName={organizationName || ""}
status={<IdPayCardStatus now={new Date()} initiative={initiative} />}
counters={getInitiativeCounters(initiative)}
actions={getInitiativeFooterProps(initiativeRewardType)}
>
<IdPayInitiativeLastUpdateCounter lastUpdateDate={lastCounterUpdate} />
{getInitiativeDetailsContent(initiative)}
{discountBottomSheet.bottomSheet}
</BonusCardScreenComponent>
);
};
const styles = StyleSheet.create({
newInitiativeMessageContainer: {
alignItems: "center",
justifyContent: "center",
padding: 32,
flex: 1,
flexGrow: 1
}
});
export { IdPayInitiativeDetailsScreen };
export function IdPayCardStatus({
now,
initiative
}: {
now: Date;
initiative: InitiativeDTO;
}) {
const getInitiativeStatus = (): BonusStatus => {
if (initiative.status === StatusEnum.UNSUBSCRIBED) {
return "REMOVED";
}
if (now > initiative.endDate) {
return "EXPIRED";
}
const next7Days = new Date(new Date(now).setDate(now.getDate() + 7));
if (next7Days > initiative.endDate) {
return "EXPIRING";
}
return "ACTIVE";
};
switch (getInitiativeStatus()) {
case "ACTIVE":
return (
<Chip color="grey-650">
{I18n.t("bonusCard.validUntil", {
endDate: format(initiative.endDate, "DD/MM/YY")
})}
</Chip>
);
case "EXPIRING":
return (
<Tag
variant="warning"
text={I18n.t("bonusCard.expiring", {
endDate: format(initiative.endDate, "DD/MM/YY")
})}
/>
);
case "EXPIRED":
return (
<Tag
variant="error"
text={I18n.t("bonusCard.expired", {
endDate: format(initiative.endDate, "DD/MM/YY")
})}
/>
);
case "PAUSED":
return <Tag variant="info" text={I18n.t("bonusCard.paused")} />;
case "REMOVED":
return <Tag variant="error" text={I18n.t("bonusCard.removed")} />;
}
}