diff --git a/locales/en/index.yml b/locales/en/index.yml
index 23d8a154ae7..0b3426af6b0 100644
--- a/locales/en/index.yml
+++ b/locales/en/index.yml
@@ -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
diff --git a/locales/it/index.yml b/locales/it/index.yml
index 5518485eb90..7ae11958b50 100644
--- a/locales/it/index.yml
+++ b/locales/it/index.yml
@@ -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
diff --git a/ts/features/bonus/cgn/navigation/navigator.tsx b/ts/features/bonus/cgn/navigation/navigator.tsx
index 19f381f3944..e5779b0d675 100644
--- a/ts/features/bonus/cgn/navigation/navigator.tsx
+++ b/ts/features/bonus/cgn/navigation/navigator.tsx
@@ -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,
@@ -143,6 +144,13 @@ export const CgnDetailsNavigator = () => (
presentation: "modal"
}}
/>
+
{
+ const navigate = useIONavigation();
+ const onPress = () => navigate.pop();
+ return (
+
+ );
+};
+
+export default CGNDiscountExpiredScreen;
diff --git a/ts/features/bonus/cgn/screens/discount/CgnDiscountCodeScreen.tsx b/ts/features/bonus/cgn/screens/discount/CgnDiscountCodeScreen.tsx
index ed9e9a30f21..35d06547c6a 100644
--- a/ts/features/bonus/cgn/screens/discount/CgnDiscountCodeScreen.tsx
+++ b/ts/features/bonus/cgn/screens/discount/CgnDiscountCodeScreen.tsx
@@ -1,6 +1,5 @@
-import { Second } from "@pagopa/ts-commons/lib/units";
import {
- Body,
+ FooterActions,
H1,
H2,
Icon,
@@ -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();
@@ -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();
};
@@ -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 (
+
+ );
+ }
+
if (discountCode) {
return (
<>
@@ -78,7 +115,8 @@ const CgnDiscountCodeScreen = () => {
firstAction: {
icon: "closeLarge",
accessibilityLabel: I18n.t("global.buttons.close"),
- onPress: onClose
+ onPress: onClose,
+ testID: "close-button"
}
}}
>
@@ -91,12 +129,7 @@ const CgnDiscountCodeScreen = () => {
-
+
{discountCode}
{isReady(discountOtp) && !isDiscountCodeExpired && (
@@ -111,14 +144,10 @@ const CgnDiscountCodeScreen = () => {
/>
>
)}
- {isDiscountCodeExpired && (
-
- {I18n.t(`bonus.cgn.merchantDetail.discount.expired`)}
-
- )}
{
return;
}
switch (merchantDetails?.discountCodeType) {
- case DiscountCodeTypeEnum.landingpage:
+ case DiscountCodeTypeEnum.landingpage: {
const landingPageUrl = discountDetails?.landingPageUrl;
const referer = discountDetails?.landingPageReferrer;
if (!landingPageUrl) {
@@ -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;
@@ -212,7 +216,8 @@ const CgnDiscountDetailScreen = () => {
),
onPress: onPressDiscountCode,
disabled: loading,
- loading
+ loading,
+ testID: "discount-code-button"
}
: undefined;
const secondaryAction: GradientBottomActions["secondaryActionProps"] =
@@ -220,7 +225,8 @@ const CgnDiscountDetailScreen = () => {
merchantDetails?.discountCodeType !== DiscountCodeTypeEnum.landingpage
? {
label: I18n.t(`bonus.cgn.merchantDetail.discount.secondaryCta`),
- onPress: onNavigateToDiscountUrl
+ onPress: onNavigateToDiscountUrl,
+ testID: "discount-url-button"
}
: undefined;
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/CGNDiscountExpiredScreen.test.tsx b/ts/features/bonus/cgn/screens/discount/___tests___/CGNDiscountExpiredScreen.test.tsx
new file mode 100644
index 00000000000..ae15fc4384a
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/CGNDiscountExpiredScreen.test.tsx
@@ -0,0 +1,49 @@
+import { fireEvent } from "@testing-library/react-native";
+import { createStore } from "redux";
+import { applicationChangeState } from "../../../../../../store/actions/application";
+import { appReducer } from "../../../../../../store/reducers";
+import { renderScreenWithNavigationStoreContext } from "../../../../../../utils/testWrapper";
+import CGN_ROUTES from "../../../navigation/routes";
+import CGNDiscountExpiredScreen from "../CGNDiscountExpiredScreen";
+
+const renderComponent = () => {
+ const globalState = appReducer(undefined, applicationChangeState("active"));
+ const store = createStore(appReducer, globalState as any);
+
+ return renderScreenWithNavigationStoreContext(
+ CGNDiscountExpiredScreen,
+ CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE_FAILURE,
+ {},
+ store
+ );
+};
+
+const mockNavigate = jest.fn();
+const mockPop = jest.fn();
+
+jest.mock("@react-navigation/native", () => ({
+ ...jest.requireActual(
+ "@react-navigation/native"
+ ),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ pop: mockPop
+ })
+}));
+
+describe("CGNDiscountExpiredScreen", () => {
+ it("should render correctly", () => {
+ const component = renderComponent();
+
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("should navigate back", () => {
+ const component = renderComponent();
+
+ const back = component.getByTestId("close-button");
+ fireEvent.press(back);
+
+ expect(mockPop).toHaveBeenCalled();
+ });
+});
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountCodeScreen.test.tsx b/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountCodeScreen.test.tsx
new file mode 100644
index 00000000000..97453a0f51c
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountCodeScreen.test.tsx
@@ -0,0 +1,98 @@
+import { fireEvent } from "@testing-library/react-native";
+import { createStore } from "redux";
+import { OtpCode } from "../../../../../../../definitions/cgn/OtpCode";
+import { remoteReady } from "../../../../../../common/model/RemoteValue";
+import { applicationChangeState } from "../../../../../../store/actions/application";
+import { appReducer } from "../../../../../../store/reducers";
+import { GlobalState } from "../../../../../../store/reducers/types";
+import { renderScreenWithNavigationStoreContext } from "../../../../../../utils/testWrapper";
+import CGN_ROUTES from "../../../navigation/routes";
+import CgnDiscountCodeScreen from "../CgnDiscountCodeScreen";
+
+const renderComponent = (state: GlobalState) => {
+ const store = createStore(appReducer, state as any);
+
+ return renderScreenWithNavigationStoreContext(
+ CgnDiscountCodeScreen,
+ CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE,
+ {},
+ store
+ );
+};
+
+const mockNavigate = jest.fn();
+const mockPop = jest.fn();
+const mockSetOptions = jest.fn();
+
+jest.mock("@react-navigation/native", () => ({
+ ...jest.requireActual(
+ "@react-navigation/native"
+ ),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ pop: mockPop,
+ setOptions: mockSetOptions
+ })
+}));
+
+const globalState = appReducer(undefined, applicationChangeState("active"));
+
+const defaultState: GlobalState = {
+ ...globalState,
+ bonus: {
+ ...globalState.bonus,
+ cgn: {
+ ...globalState.bonus.cgn,
+ otp: {
+ ...globalState.bonus.cgn.otp,
+ data: remoteReady({
+ code: "123456" as OtpCode,
+ expires_at: new Date(),
+ ttl: 100
+ })
+ }
+ }
+ }
+};
+
+describe("CgnDiscountCodeScreen", () => {
+ it("should render correctly", () => {
+ const component = renderComponent(defaultState);
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("should navigate back", () => {
+ const component = renderComponent(defaultState);
+
+ const back = component.getByTestId("close-button");
+ fireEvent.press(back);
+
+ expect(mockPop).toHaveBeenCalled();
+ });
+
+ it("should generate new discount code", () => {
+ const state = {
+ ...defaultState,
+ bonus: {
+ ...defaultState.bonus,
+ cgn: {
+ ...defaultState.bonus.cgn,
+ otp: {
+ ...defaultState.bonus.cgn.otp,
+ data: remoteReady({
+ code: "123456" as OtpCode,
+ expires_at: new Date(),
+ ttl: 0
+ })
+ }
+ }
+ }
+ };
+ const component = renderComponent(state);
+
+ const generateButton = component.getByTestId("generate-button");
+ fireEvent.press(generateButton);
+
+ expect(mockNavigate).not.toHaveBeenCalled();
+ });
+});
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountDetailScreen.test.tsx b/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountDetailScreen.test.tsx
new file mode 100644
index 00000000000..e4a0c78ba67
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/CgnDiscountDetailScreen.test.tsx
@@ -0,0 +1,160 @@
+import { NonEmptyString } from "@pagopa/ts-commons/lib/strings";
+import { fireEvent } from "@testing-library/react-native";
+import { createStore } from "redux";
+import { Discount } from "../../../../../../../definitions/cgn/merchants/Discount";
+import { DiscountCodeTypeEnum } from "../../../../../../../definitions/cgn/merchants/DiscountCodeType";
+import { Merchant } from "../../../../../../../definitions/cgn/merchants/Merchant";
+import { SupportTypeEnum } from "../../../../../../../definitions/cgn/merchants/SupportType";
+import { remoteReady } from "../../../../../../common/model/RemoteValue";
+import { applicationChangeState } from "../../../../../../store/actions/application";
+import { appReducer } from "../../../../../../store/reducers";
+import { GlobalState } from "../../../../../../store/reducers/types";
+import { renderScreenWithNavigationStoreContext } from "../../../../../../utils/testWrapper";
+import CGN_ROUTES from "../../../navigation/routes";
+import { setMerchantDiscountCode } from "../../../store/actions/merchants";
+import CgnDiscountDetailScreen from "../CgnDiscountDetailScreen";
+
+const renderComponent = (state: GlobalState) => {
+ const store = createStore(appReducer, state as any);
+
+ return {
+ component: renderScreenWithNavigationStoreContext(
+ CgnDiscountDetailScreen,
+ CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT,
+ {},
+ store
+ ),
+ store
+ };
+};
+
+const mockNavigate = jest.fn();
+
+jest.mock("@react-navigation/native", () => ({
+ ...jest.requireActual(
+ "@react-navigation/native"
+ ),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ setOptions: jest.fn()
+ })
+}));
+
+const discountDetailsMock: Discount = {
+ id: "12345" as NonEmptyString,
+ name: "Test Discount" as NonEmptyString,
+ startDate: new Date(),
+ endDate: new Date(),
+ productCategories: [],
+ description: "Description of test discount" as NonEmptyString,
+ discount: 10,
+ condition: "Test condition" as NonEmptyString,
+ discountUrl: "https://example.com" as NonEmptyString
+};
+
+const mockSelectedMerchant: Merchant = {
+ id: "12345" as NonEmptyString,
+ name: "Test Merchant" as NonEmptyString,
+ description: "Description of test merchant" as NonEmptyString,
+ websiteUrl: "https://example.com" as NonEmptyString,
+ allNationalAddresses: false,
+ discounts: [],
+ supportType: SupportTypeEnum.EMAILADDRESS,
+ supportValue: "10" as NonEmptyString,
+ discountCodeType: DiscountCodeTypeEnum.api
+};
+
+const globalState = appReducer(undefined, applicationChangeState("active"));
+
+describe("CgnDiscountDetailScreen", () => {
+ it("should render correctly", () => {
+ const { component } = renderComponent(globalState);
+
+ expect(component.toJSON()).toMatchSnapshot();
+ });
+
+ it("should navigate to discount code screen on button press", async () => {
+ const initialState = {
+ ...globalState,
+ bonus: {
+ ...globalState.bonus,
+ cgn: {
+ ...globalState.bonus.cgn,
+ merchants: {
+ ...globalState.bonus.cgn.merchants,
+ selectedDiscount: remoteReady(discountDetailsMock),
+ selectedMerchant: remoteReady(mockSelectedMerchant)
+ }
+ }
+ }
+ };
+
+ const { component, store } = renderComponent(initialState);
+
+ store.dispatch(setMerchantDiscountCode("12345"));
+
+ fireEvent.press(component.getByTestId("discount-code-button"));
+
+ expect(mockNavigate).toHaveBeenCalledWith(
+ CGN_ROUTES.DETAILS.MERCHANTS.DISCOUNT_CODE
+ );
+ });
+
+ it("should show footer actions if applicable", () => {
+ const initialState = {
+ ...globalState,
+ bonus: {
+ ...globalState.bonus,
+ cgn: {
+ ...globalState.bonus.cgn,
+ merchants: {
+ ...globalState.bonus.cgn.merchants,
+ selectedDiscount: remoteReady(discountDetailsMock),
+ selectedMerchant: remoteReady(mockSelectedMerchant)
+ }
+ }
+ }
+ };
+
+ const { component } = renderComponent(initialState);
+
+ expect(component.getByTestId("discount-code-button")).toBeTruthy();
+ expect(component.getByTestId("discount-url-button")).toBeTruthy();
+ });
+
+ it("should navigate to landing page webview on button press", async () => {
+ const initialState = {
+ ...globalState,
+ bonus: {
+ ...globalState.bonus,
+ cgn: {
+ ...globalState.bonus.cgn,
+ merchants: {
+ ...globalState.bonus.cgn.merchants,
+ selectedDiscount: remoteReady({
+ ...discountDetailsMock,
+ landingPageUrl: "https://example.com/landing" as NonEmptyString,
+ landingPageReferrer: "referrer" as NonEmptyString
+ }),
+ selectedMerchant: remoteReady({
+ ...mockSelectedMerchant,
+ discountCodeType: DiscountCodeTypeEnum.landingpage
+ })
+ }
+ }
+ }
+ };
+
+ const { component } = renderComponent(initialState);
+
+ fireEvent.press(component.getByTestId("discount-code-button"));
+
+ expect(mockNavigate).toHaveBeenCalledWith(CGN_ROUTES.DETAILS.MAIN, {
+ screen: CGN_ROUTES.DETAILS.MERCHANTS.LANDING_WEBVIEW,
+ params: {
+ landingPageUrl: "https://example.com/landing",
+ landingPageReferrer: "referrer"
+ }
+ });
+ });
+});
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CGNDiscountExpiredScreen.test.tsx.snap b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CGNDiscountExpiredScreen.test.tsx.snap
new file mode 100644
index 00000000000..114ba132127
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CGNDiscountExpiredScreen.test.tsx.snap
@@ -0,0 +1,730 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CGNDiscountExpiredScreen should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CGN_MERCHANTS_DISCOUNT_CODE_FAILURE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ In questo momento non è possibile generare un codice
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountCodeScreen.test.tsx.snap b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountCodeScreen.test.tsx.snap
new file mode 100644
index 00000000000..09760fa0a90
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountCodeScreen.test.tsx.snap
@@ -0,0 +1,827 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CgnDiscountCodeScreen should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CGN_MERCHANTS_DISCOUNT_CODE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Il codice è scaduto
+
+
+
+
+
+
+
+
+ Usa un nuovo codice
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountDetailScreen.test.tsx.snap b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountDetailScreen.test.tsx.snap
new file mode 100644
index 00000000000..87c684845df
--- /dev/null
+++ b/ts/features/bonus/cgn/screens/discount/___tests___/__snapshots__/CgnDiscountDetailScreen.test.tsx.snap
@@ -0,0 +1,344 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CgnDiscountDetailScreen should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CGN_MERCHANTS_DISCOUNT_SCREEN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;