Skip to content

Commit

Permalink
chore: [PE-803] CGN discount outcome screen (#6470)
Browse files Browse the repository at this point in the history
## Short description
This pull request introduces functionality for generating a new discount
code when the previous one has expired with error handling

## List of changes proposed in this pull request
- Added new routes for the expired discount code screen in the
navigation files
- Updated `CgnDiscountCodeScreen` to include logic for generating new
discount codes and handling expired codes
- Replace legacy `FooterActions` with `io-app-design-system` component
- Reset discount code value at timer expiration 

## How to test
(From dev, for better test experience, ensure `discountTypes` is a
single array containing only the value `api`)

- Try to get a discount code with expiration 
- After the timer expires, ensure the appropriate outcome screen is
displayed correctly
- Verify if a new discount code is generated pressing on `Usa un nuovo
codice`
- Confirm the system navigates to the correct error screen when a
discount code cannot be generated pressing `Usa un nuovo codice`

## Preview

https://github.com/user-attachments/assets/6ec8010a-654c-445d-ba1e-782d8a8b8b1b
  • Loading branch information
LeleDallas authored Dec 3, 2024
1 parent e005566 commit 876403a
Show file tree
Hide file tree
Showing 14 changed files with 2,327 additions and 47 deletions.
4 changes: 3 additions & 1 deletion locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2792,10 +2792,12 @@ bonus:
static: Vai al codice sconto
bucket: Vai al codice sconto
landingpage: Vai all'opportunità
createNew: Usa un nuovo codice
secondaryCta: Vai all'opportunità
title: Ecco il codice sconto!
expired: Codice sconto scaduto!
expired: Il codice è scaduto
copyButton: Copia codice sconto
error: In questo momento non è possibile generare un codice
cta:
activeBonus: Activate Carta Giovani Nazionale
back: Not now
Expand Down
4 changes: 3 additions & 1 deletion locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2792,10 +2792,12 @@ bonus:
static: Vai al codice sconto
bucket: Vai al codice sconto
landingpage: Vai all'opportunità
createNew: Usa un nuovo codice
secondaryCta: Vai all'opportunità
title: Ecco il codice sconto!
expired: Codice sconto scaduto!
expired: Il codice è scaduto
copyButton: Copia codice sconto
error: In questo momento non è possibile generare un codice
cta:
activeBonus: Attiva Carta Giovani Nazionale
back: Non ora
Expand Down
14 changes: 11 additions & 3 deletions ts/features/bonus/cgn/navigation/navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ import CgnActivationTimeoutScreen from "../screens/activation/CgnActivationTimeo
import CgnAlreadyActiveScreen from "../screens/activation/CgnAlreadyActiveScreen";
import CgnCTAStartActivationScreen from "../screens/activation/CgnCTAStartActivationScreen";
import CgnInformationScreen from "../screens/activation/CgnInformationScreen";
import CGNDiscountExpiredScreen from "../screens/discount/CGNDiscountExpiredScreen";
import CgnDiscountCodeScreen from "../screens/discount/CgnDiscountCodeScreen";
import CgnDiscountDetailScreen from "../screens/discount/CgnDiscountDetailScreen";
import EycaActivationLoading from "../screens/eyca/activation/EycaActivationLoading";
import CgnMerchantDetailScreen from "../screens/merchants/CgnMerchantDetailScreen";
import CgnMerchantLandingWebview from "../screens/merchants/CgnMerchantLandingWebview";
import { CgnMerchantSearchScreen } from "../screens/merchants/CgnMerchantSearchScreen";
import CgnMerchantsCategoriesSelectionScreen from "../screens/merchants/CgnMerchantsCategoriesSelectionScreen";
import CgnMerchantsListByCategory from "../screens/merchants/CgnMerchantsListByCategory";
import CgnMerchantsTabsScreen from "../screens/merchants/CgnMerchantsTabsScreen";
import CgnDiscountDetailScreen from "../screens/discount/CgnDiscountDetailScreen";
import CgnDiscountCodeScreen from "../screens/discount/CgnDiscountCodeScreen";
import { CgnMerchantSearchScreen } from "../screens/merchants/CgnMerchantSearchScreen";
import {
CgnActivationParamsList,
CgnDetailsParamsList,
Expand Down Expand Up @@ -143,6 +144,13 @@ export const CgnDetailsNavigator = () => (
presentation: "modal"
}}
/>
<DetailStack.Screen
name={CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE_FAILURE}
component={CGNDiscountExpiredScreen}
options={{
headerShown: false
}}
/>
<DetailStack.Screen
name={CGN_ROUTES.DETAILS.MERCHANTS.SEARCH}
component={CgnMerchantSearchScreen}
Expand Down
1 change: 1 addition & 0 deletions ts/features/bonus/cgn/navigation/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type CgnDetailsParamsList = {
.LANDING_WEBVIEW]: CgnMerchantLandingWebviewNavigationParams;
[CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT]: undefined;
[CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE]: undefined;
[CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE_FAILURE]: undefined;
[CGN_ROUTES.DETAILS.MERCHANTS.SEARCH]: undefined;
};

Expand Down
1 change: 1 addition & 0 deletions ts/features/bonus/cgn/navigation/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const CGN_ROUTES = {
LANDING_WEBVIEW: "CGN_MERCHANTS_LANDING_WEBVIEW",
DISCOUNT: "CGN_MERCHANTS_DISCOUNT_SCREEN",
DISCOUNT_CODE: "CGN_MERCHANTS_DISCOUNT_CODE",
DISCOUNT_CODE_FAILURE: "CGN_MERCHANTS_DISCOUNT_CODE_FAILURE",
SEARCH: "CGN_MERCHANTS_SEARCH"
} as const
} as const
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent";
import { useIONavigation } from "../../../../../navigation/params/AppParamsList";
import I18n from "../../../../../i18n";

const CGNDiscountExpiredScreen = () => {
const navigate = useIONavigation();
const onPress = () => navigate.pop();
return (
<OperationResultScreenContent
pictogram="umbrellaNew"
title={I18n.t("bonus.cgn.merchantDetail.discount.error")}
isHeaderVisible={false}
action={{
label: I18n.t("global.buttons.close"),
onPress,
testID: "close-button"
}}
/>
);
};

export default CGNDiscountExpiredScreen;
71 changes: 50 additions & 21 deletions ts/features/bonus/cgn/screens/discount/CgnDiscountCodeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Second } from "@pagopa/ts-commons/lib/units";
import {
Body,
FooterActions,
H1,
H2,
Icon,
Expand All @@ -9,19 +8,23 @@ import {
useIOTheme,
VSpacer
} from "@pagopa/io-app-design-system";
import { Second } from "@pagopa/ts-commons/lib/units";
import * as React from "react";
import { StyleSheet, View } from "react-native";
import { useIOSelector } from "../../../../../store/hooks";
import { cgnSelectedDiscountCodeSelector } from "../../store/reducers/merchants";
import { Otp } from "../../../../../../definitions/cgn/Otp";
import { isReady } from "../../../../../common/model/RemoteValue";
import { OperationResultScreenContent } from "../../../../../components/screens/OperationResultScreenContent";
import { IOScrollView } from "../../../../../components/ui/IOScrollView";
import I18n from "../../../../../i18n";
import { useIONavigation } from "../../../../../navigation/params/AppParamsList";
import { IOScrollView } from "../../../../../components/ui/IOScrollView";
import { FooterActions } from "../../../../../components/ui/FooterActions";
import { useIODispatch, useIOSelector } from "../../../../../store/hooks";
import { clipboardSetStringWithFeedback } from "../../../../../utils/clipboard";
import { CgnDiscountExpireProgressBar } from "../../components/merchants/discount/CgnDiscountExpireProgressBar";
import CGN_ROUTES from "../../navigation/routes";
import { resetMerchantDiscountCode } from "../../store/actions/merchants";
import { cgnGenerateOtp } from "../../store/actions/otp";
import { cgnSelectedDiscountCodeSelector } from "../../store/reducers/merchants";
import { cgnOtpDataSelector } from "../../store/reducers/otp";
import { isReady } from "../../../../../common/model/RemoteValue";
import { Otp } from "../../../../../../definitions/cgn/Otp";

const getOtpTTL = (otp: Otp): Second => {
const now = new Date();
Expand All @@ -42,14 +45,25 @@ const getOtpExpirationTotal = (otp: Otp): Second =>
) as Second;

const CgnDiscountCodeScreen = () => {
const dispatch = useIODispatch();
const discountCode = useIOSelector(cgnSelectedDiscountCodeSelector);
const discountOtp = useIOSelector(cgnOtpDataSelector);
const [isDiscountCodeExpired, setIsDiscountCodeExpired] =
React.useState(false);

const navigation = useIONavigation();
const theme = useIOTheme();

const generateNewDiscountCode = () => {
dispatch(
cgnGenerateOtp.request({
onSuccess: () => null,
onError: () =>
navigation.navigate(CGN_ROUTES.DETAILS.MAIN, {
screen: CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE_FAILURE
})
})
);
};
const onClose = () => {
navigation.pop();
};
Expand All @@ -67,6 +81,29 @@ const CgnDiscountCodeScreen = () => {
}
}, [discountOtp]);

if (isDiscountCodeExpired) {
// reset discount code if expired to avoid showing it again when server is down
dispatch(resetMerchantDiscountCode());
return (
<OperationResultScreenContent
testID="expired-screen"
pictogram="timing"
title={I18n.t("bonus.cgn.merchantDetail.discount.expired")}
isHeaderVisible
action={{
label: I18n.t("bonus.cgn.merchantDetail.discount.cta.createNew"),
onPress: generateNewDiscountCode,
testID: "generate-button"
}}
secondaryAction={{
label: I18n.t("global.buttons.close"),
onPress: onClose,
testID: "close-button"
}}
/>
);
}

if (discountCode) {
return (
<>
Expand All @@ -78,7 +115,8 @@ const CgnDiscountCodeScreen = () => {
firstAction: {
icon: "closeLarge",
accessibilityLabel: I18n.t("global.buttons.close"),
onPress: onClose
onPress: onClose,
testID: "close-button"
}
}}
>
Expand All @@ -91,12 +129,7 @@ const CgnDiscountCodeScreen = () => {
<Icon name="tag" color="grey-300" />
</View>
<VSpacer size={4} />
<H1
textStyle={StyleSheet.flatten([
styles.labelCode,
isDiscountCodeExpired && { textDecorationLine: "line-through" }
])}
>
<H1 textStyle={StyleSheet.flatten([styles.labelCode])}>
{discountCode}
</H1>
{isReady(discountOtp) && !isDiscountCodeExpired && (
Expand All @@ -111,14 +144,10 @@ const CgnDiscountCodeScreen = () => {
/>
</>
)}
{isDiscountCodeExpired && (
<Body style={IOStyles.selfCenter}>
{I18n.t(`bonus.cgn.merchantDetail.discount.expired`)}
</Body>
)}
</View>
</IOScrollView>
<FooterActions
testID="copy-button"
actions={{
type: "SingleButton",
primary: {
Expand Down
48 changes: 27 additions & 21 deletions ts/features/bonus/cgn/screens/discount/CgnDiscountDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as pot from "@pagopa/ts-commons/lib/pot";
import {
buttonSolidHeight,
GradientBottomActions,
Expand All @@ -7,7 +6,10 @@ import {
IOToast,
IOVisualCostants
} from "@pagopa/io-app-design-system";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { useNavigation } from "@react-navigation/native";
import * as React from "react";
import { LayoutChangeEvent, Platform, StyleSheet, View } from "react-native";
import Animated, {
Easing,
useAnimatedScrollHandler,
Expand All @@ -16,36 +18,34 @@ import Animated, {
withTiming
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import { LayoutChangeEvent, Platform, StyleSheet, View } from "react-native";
import { DiscountCodeTypeEnum } from "../../../../../../definitions/cgn/merchants/DiscountCodeType";
import { isLoading, isReady } from "../../../../../common/model/RemoteValue";
import { useIODispatch, useIOSelector } from "../../../../../store/hooks";
import {
cgnSelectedDiscountCodeSelector,
cgnSelectedDiscountSelector,
cgnSelectedMerchantSelector
} from "../../store/reducers/merchants";
import { useHeaderSecondLevel } from "../../../../../hooks/useHeaderSecondLevel";
import { useScreenEndMargin } from "../../../../../hooks/useScreenEndMargin";
import { CgnDiscountHeader } from "../../components/merchants/discount/CgnDiscountHeader";
import { CgnDiscountContent } from "../../components/merchants/discount/CgnDiscountContent";
import I18n from "../../../../../i18n";
import { DiscountCodeTypeEnum } from "../../../../../../definitions/cgn/merchants/DiscountCodeType";
import { mixpanelTrack } from "../../../../../mixpanel";
import { getCgnUserAgeRange } from "../../utils/dates";
import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList";
import { useIODispatch, useIOSelector } from "../../../../../store/hooks";
import { profileSelector } from "../../../../../store/reducers/profile";
import { openWebUrl } from "../../../../../utils/url";
import { IOStackNavigationProp } from "../../../../../navigation/params/AppParamsList";
import { CgnDiscountContent } from "../../components/merchants/discount/CgnDiscountContent";
import { CgnDiscountHeader } from "../../components/merchants/discount/CgnDiscountHeader";
import { CgnDetailsParamsList } from "../../navigation/params";
import CGN_ROUTES from "../../navigation/routes";
import { cgnCodeFromBucket } from "../../store/actions/bucket";
import {
resetMerchantDiscountCode,
setMerchantDiscountCode
} from "../../store/actions/merchants";
import { cgnCodeFromBucket } from "../../store/actions/bucket";
import { cgnBucketSelector } from "../../store/reducers/bucket";
import { CgnDetailsParamsList } from "../../navigation/params";
import { cgnGenerateOtp, resetOtpState } from "../../store/actions/otp";
import { cgnBucketSelector } from "../../store/reducers/bucket";
import {
cgnSelectedDiscountCodeSelector,
cgnSelectedDiscountSelector,
cgnSelectedMerchantSelector
} from "../../store/reducers/merchants";
import { cgnOtpDataSelector } from "../../store/reducers/otp";
import { getCgnUserAgeRange } from "../../utils/dates";

const gradientSafeAreaHeight: IOSpacingScale = 96;

Expand Down Expand Up @@ -140,7 +140,7 @@ const CgnDiscountDetailScreen = () => {
return;
}
switch (merchantDetails?.discountCodeType) {
case DiscountCodeTypeEnum.landingpage:
case DiscountCodeTypeEnum.landingpage: {
const landingPageUrl = discountDetails?.landingPageUrl;
const referer = discountDetails?.landingPageReferrer;
if (!landingPageUrl) {
Expand All @@ -155,12 +155,16 @@ const CgnDiscountDetailScreen = () => {
}
});
break;
}
case DiscountCodeTypeEnum.api:
mixpanelCgnEvent("CGN_OTP_START_REQUEST");
dispatch(
cgnGenerateOtp.request({
onSuccess: navigateToDiscountCode,
onError: showErrorToast
onError: () =>
navigation.navigate(CGN_ROUTES.DETAILS.MAIN, {
screen: CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE_FAILURE
})
})
);
break;
Expand Down Expand Up @@ -212,15 +216,17 @@ const CgnDiscountDetailScreen = () => {
),
onPress: onPressDiscountCode,
disabled: loading,
loading
loading,
testID: "discount-code-button"
}
: undefined;
const secondaryAction: GradientBottomActions["secondaryActionProps"] =
discountDetails?.discountUrl &&
merchantDetails?.discountCodeType !== DiscountCodeTypeEnum.landingpage
? {
label: I18n.t(`bonus.cgn.merchantDetail.discount.secondaryCta`),
onPress: onNavigateToDiscountUrl
onPress: onNavigateToDiscountUrl,
testID: "discount-url-button"
}
: undefined;

Expand Down
Loading

0 comments on commit 876403a

Please sign in to comment.