diff --git a/ts/components/messages/MessageDetailCTABar.tsx b/ts/components/messages/MessageDetailCTABar.tsx index 5ea83df5d28..f0e579ede46 100644 --- a/ts/components/messages/MessageDetailCTABar.tsx +++ b/ts/components/messages/MessageDetailCTABar.tsx @@ -7,7 +7,7 @@ import { CreatedMessageWithContent } from "../../../definitions/backend/CreatedM import { ServicePublic } from "../../../definitions/backend/ServicePublic"; import { ReduxProps } from "../../store/actions/types"; import { PaidReason } from "../../store/reducers/entities/payments"; -import { isExpired, paymentExpirationInfo } from "../../utils/messages"; +import { getCTA, isExpired, paymentExpirationInfo } from "../../utils/messages"; import CalendarEventButton from "./CalendarEventButton"; import { MessageNestedCTABar } from "./MessageNestedCTABar"; import PaymentButton from "./PaymentButton"; @@ -89,16 +89,14 @@ class MessageDetailCTABar extends React.PureComponent { {paymentButton} ); - const nestedCTAs = ( - - ); - const footer2 = nestedCTAs && ( + const maybeCtas = getCTA(this.props.message); + const footer2 = maybeCtas.isSome() && ( - {nestedCTAs} + ); return ( diff --git a/ts/components/messages/MessageListCTABar.tsx b/ts/components/messages/MessageListCTABar.tsx index 5fd4af00cf4..e827564efe4 100644 --- a/ts/components/messages/MessageListCTABar.tsx +++ b/ts/components/messages/MessageListCTABar.tsx @@ -10,7 +10,7 @@ import { ReduxProps } from "../../store/actions/types"; import { PaidReason } from "../../store/reducers/entities/payments"; import customVariables from "../../theme/variables"; import { formatDateAsDay, formatDateAsMonth } from "../../utils/dates"; -import { isExpired, paymentExpirationInfo } from "../../utils/messages"; +import { getCTA, isExpired, paymentExpirationInfo } from "../../utils/messages"; import CalendarEventButton from "./CalendarEventButton"; import CalendarIconComponent from "./CalendarIconComponent"; import { MessageNestedCTABar } from "./MessageNestedCTABar"; @@ -126,14 +126,16 @@ class MessageListCTABar extends React.PureComponent { public render() { const calendarIcon = this.renderCalendarIcon(); const calendarEventButton = this.renderCalendarEventButton(); + const maybeCTA = getCTA(this.props.message); // payment CTA has priority to nested CTA - const nestedCTA = !this.hasPaymentData ? ( - - ) : null; + const nestedCTA = + !this.hasPaymentData && maybeCTA.isSome() ? ( + + ) : null; const content = nestedCTA || ( <> {calendarIcon} diff --git a/ts/components/messages/MessageNestedCTABar.tsx b/ts/components/messages/MessageNestedCTABar.tsx index 30c444d5542..390493a0e09 100644 --- a/ts/components/messages/MessageNestedCTABar.tsx +++ b/ts/components/messages/MessageNestedCTABar.tsx @@ -1,18 +1,12 @@ import { View } from "native-base"; -import React from "react"; +import React, { ReactElement } from "react"; import { Dispatch } from "redux"; -import { CreatedMessageWithContent } from "../../../definitions/backend/CreatedMessageWithContent"; -import { CTA } from "../../types/MessageCTA"; -import { - getCTA, - handleCtaAction, - hasCTAValidActions, - isCtaActionValid -} from "../../utils/messages"; +import { CTA, CTAS } from "../../types/MessageCTA"; +import { handleCtaAction, isCtaActionValid } from "../../utils/messages"; import { MessageNestedCtaButton } from "./MessageNestedCtaButton"; type Props = { - message: CreatedMessageWithContent; + ctas: CTAS; xsmall: boolean; dispatch: Dispatch; }; @@ -20,40 +14,33 @@ type Props = { // render cta1 and cta2 if they are defined in the message content as nested front-matter export const MessageNestedCTABar: React.FunctionComponent = ( props: Props -) => { +): ReactElement => { const handleCTAPress = (cta: CTA) => { handleCtaAction(cta, props.dispatch); }; - - const maybeNestedCTA = getCTA(props.message); - if (maybeNestedCTA.isSome()) { - const ctas = maybeNestedCTA.value; - if (hasCTAValidActions(ctas)) { - const cta2 = ctas.cta_2 && - isCtaActionValid(ctas.cta_2) && ( - - ); - const cta1 = isCtaActionValid(ctas.cta_1) && ( - - ); - return ( - <> - {cta2} - {cta2 && } - {cta1} - - ); - } - } - return null; + const { ctas } = props; + const cta2 = ctas.cta_2 && + isCtaActionValid(ctas.cta_2) && ( + + ); + const cta1 = isCtaActionValid(ctas.cta_1) && ( + + ); + return ( + <> + {cta2} + {cta2 && } + {cta1} + + ); }; diff --git a/ts/features/bonusVacanze/store/sagas/activation/__tests__/integration/mockData.ts b/ts/features/bonusVacanze/store/sagas/activation/__tests__/integration/mockData.ts index 9eab64f8bd5..a77373a5819 100644 --- a/ts/features/bonusVacanze/store/sagas/activation/__tests__/integration/mockData.ts +++ b/ts/features/bonusVacanze/store/sagas/activation/__tests__/integration/mockData.ts @@ -2,11 +2,13 @@ import { Either, left, right } from "fp-ts/lib/Either"; import { Errors } from "io-ts"; import { pot } from "italia-ts-commons"; import { ProblemJson } from "italia-ts-commons/lib/responses"; +import { BonusActivationStatusEnum } from "../../../../../../../../definitions/bonus_vacanze/BonusActivationStatus"; import { InstanceId } from "../../../../../../../../definitions/bonus_vacanze/InstanceId"; import { navigationHistoryPop } from "../../../../../../../store/actions/navigationHistory"; import { mockedBonus } from "../../../../../mock/mockData"; import { navigateToBonusActivationCompleted, + navigateToBonusActivationTimeout, navigateToBonusActiveDetailScreen, navigateToBonusAlreadyExists, navigateToEligibilityExpired @@ -39,6 +41,11 @@ const startActivationRequestCreated: Either = right({ status: 201, value: { id: "bonus_id" } as InstanceId }); + +const startActivationProcessingRequest: Either = right({ + status: 202 +}); + const startActivationEligibilityExpired: Either = right({ status: 403 }); @@ -53,6 +60,14 @@ const getActivationSuccess: Either = right({ value: mockedBonus }); +const getActivationSuccessBonusError: Either = right({ + status: 200, + value: { + ...mockedBonus, + status: BonusActivationStatusEnum.FAILED + } +}); + const getActivationNoToken: Either = right({ status: 401 }); @@ -75,7 +90,6 @@ interface MockBackendScenario extends IExpectedActions { finalState: MockActivationState; } -// TODO: test polling timeout case export const success: MockBackendScenario = { displayName: "success", responses: [ @@ -125,6 +139,25 @@ export const bonusAlreadyExists: MockBackendScenario = { } }; +// TODO: add others timeout case +export const timeout: MockBackendScenario = { + displayName: "timeout", + responses: [ + { + startBonusActivationResponse: startActivationProcessingRequest, + getBonusActivationResponseById: right(undefined) + } + ], + expectedActions: [ + navigateToBonusActivationTimeout(), + navigationHistoryPop(1) + ], + finalState: { + activation: { status: BonusActivationProgressEnum.TIMEOUT }, + allActive: {} + } +}; + export const error: MockBackendScenario = { displayName: "error", responses: [ @@ -155,6 +188,10 @@ export const error: MockBackendScenario = { { startBonusActivationResponse: startActivationRequestCreated, getBonusActivationResponseById: genericDecodingFailure + }, + { + startBonusActivationResponse: startActivationRequestCreated, + getBonusActivationResponseById: getActivationSuccessBonusError } ], expectedActions: [], @@ -168,6 +205,7 @@ export const backendIntegrationTestCases: ReadonlyArray = [ success, eligibilityExpired, bonusAlreadyExists, + timeout, error ]; diff --git a/ts/features/bonusVacanze/store/sagas/activation/getBonusActivationSaga.ts b/ts/features/bonusVacanze/store/sagas/activation/getBonusActivationSaga.ts index 8b055b7af01..29653ab2fbb 100644 --- a/ts/features/bonusVacanze/store/sagas/activation/getBonusActivationSaga.ts +++ b/ts/features/bonusVacanze/store/sagas/activation/getBonusActivationSaga.ts @@ -1,11 +1,12 @@ import { Either, left, right } from "fp-ts/lib/Either"; import { none, Option, some } from "fp-ts/lib/Option"; -import { readableReport } from "italia-ts-commons/lib/reporters"; import { Millisecond } from "italia-ts-commons/lib/units"; import { call, Effect } from "redux-saga/effects"; import { ActionType } from "typesafe-actions"; +import { BonusActivationStatusEnum } from "../../../../../../definitions/bonus_vacanze/BonusActivationStatus"; import { BonusActivationWithQrCode } from "../../../../../../definitions/bonus_vacanze/BonusActivationWithQrCode"; import { SagaCallReturnType } from "../../../../../types/utils"; +import { readablePrivacyReport } from "../../../../../utils/reporters"; import { startTimer } from "../../../../../utils/timer"; import { BackendBonusVacanze } from "../../../api/backendBonusVacanze"; import { activateBonusVacanze } from "../../actions/bonusVacanze"; @@ -44,7 +45,18 @@ function* getBonusActivation( if (getLatestBonusVacanzeFromIdResult.isRight()) { // 200 -> we got the check result, polling must be stopped if (getLatestBonusVacanzeFromIdResult.value.status === 200) { - return right(getLatestBonusVacanzeFromIdResult.value.value); + const activation = getLatestBonusVacanzeFromIdResult.value.value; + switch (activation.status) { + // processing -> polling should continue + case BonusActivationStatusEnum.PROCESSING: + return left(none); + case BonusActivationStatusEnum.FAILED: + // blocking error + return left(some(new Error("Bonus Activation failed"))); + default: + // active + return right(getLatestBonusVacanzeFromIdResult.value.value); + } } // Request not found - polling must be stopped if (getLatestBonusVacanzeFromIdResult.value.status === 404) { @@ -55,7 +67,9 @@ function* getBonusActivation( } else { // we got some error on decoding, stop polling return left( - some(Error(readableReport(getLatestBonusVacanzeFromIdResult.value))) + some( + Error(readablePrivacyReport(getLatestBonusVacanzeFromIdResult.value)) + ) ); } } catch (e) { @@ -120,6 +134,12 @@ export const bonusActivationSaga = ( } } } + // 202 -> still processing + if (startBonusActivationProcedureResult.value.status === 202) { + return activateBonusVacanze.success({ + status: BonusActivationProgressEnum.TIMEOUT + }); + } // 409 -> Cannot activate a new bonus because another bonus related to this user was found. // 403 -> Eligibility Expired else if (status === 409 || status === 403) { @@ -132,7 +152,9 @@ export const bonusActivationSaga = ( ); } // decoding failure - throw Error(readableReport(startBonusActivationProcedureResult.value)); + throw Error( + readablePrivacyReport(startBonusActivationProcedureResult.value) + ); } catch (e) { return activateBonusVacanze.failure(e); } diff --git a/ts/features/bonusVacanze/store/sagas/eligibility/getBonusEligibilitySaga.ts b/ts/features/bonusVacanze/store/sagas/eligibility/getBonusEligibilitySaga.ts index ecc5aacc3c0..4e5da28b81f 100644 --- a/ts/features/bonusVacanze/store/sagas/eligibility/getBonusEligibilitySaga.ts +++ b/ts/features/bonusVacanze/store/sagas/eligibility/getBonusEligibilitySaga.ts @@ -1,6 +1,5 @@ import { Either, left, right } from "fp-ts/lib/Either"; import { none, Option, some } from "fp-ts/lib/Option"; -import { readableReport } from "italia-ts-commons/lib/reporters"; import { Millisecond } from "italia-ts-commons/lib/units"; import { call, Effect, put } from "redux-saga/effects"; import { ActionType } from "typesafe-actions"; @@ -10,6 +9,7 @@ import { EligibilityCheckSuccess } from "../../../../../../definitions/bonus_vac import { EligibilityCheckSuccessConflict } from "../../../../../../definitions/bonus_vacanze/EligibilityCheckSuccessConflict"; import { EligibilityCheckSuccessEligible } from "../../../../../../definitions/bonus_vacanze/EligibilityCheckSuccessEligible"; import { SagaCallReturnType } from "../../../../../types/utils"; +import { readablePrivacyReport } from "../../../../../utils/reporters"; import { startTimer } from "../../../../../utils/timer"; import { BackendBonusVacanze } from "../../../api/backendBonusVacanze"; import { @@ -74,7 +74,9 @@ function* getCheckBonusEligibilitySaga( return left(none); } else { // we got some error on decoding, stop polling - return left(some(Error(readableReport(eligibilityCheckResult.value)))); + return left( + some(Error(readablePrivacyReport(eligibilityCheckResult.value))) + ); } } catch (e) { return left(none); @@ -107,16 +109,6 @@ export const bonusEligibilitySaga = ( Effect | ActionType > { try { - // before activate, make an optimistic check, maybe the isee result is already available - const firstCheck = yield call( - executeGetEligibilityCheck(getBonusEligibilityCheck) - ); - if (firstCheck.isRight()) { - return checkBonusVacanzeEligibility.success({ - check: firstCheck.value, - status: eligibilityResultToEnum(firstCheck.value) - }); - } const startEligibilityResult: SagaCallReturnType< typeof startBonusEligibilityCheck > = yield call(startBonusEligibilityCheck, {}); @@ -176,7 +168,7 @@ export const bonusEligibilitySaga = ( throw Error(`response status ${startEligibilityResult.value.status}`); } else { - throw Error(readableReport(startEligibilityResult.value)); + throw Error(readablePrivacyReport(startEligibilityResult.value)); } } catch (e) { return checkBonusVacanzeEligibility.failure(e); diff --git a/ts/features/bonusVacanze/store/sagas/handleLoadAllBonusActivationSaga.ts b/ts/features/bonusVacanze/store/sagas/handleLoadAllBonusActivationSaga.ts index 2467a052122..8a6601f4a32 100644 --- a/ts/features/bonusVacanze/store/sagas/handleLoadAllBonusActivationSaga.ts +++ b/ts/features/bonusVacanze/store/sagas/handleLoadAllBonusActivationSaga.ts @@ -1,7 +1,7 @@ -import { readableReport } from "italia-ts-commons/lib/reporters"; import { SagaIterator } from "redux-saga"; import { all, call, put } from "redux-saga/effects"; import { SagaCallReturnType } from "../../../../types/utils"; +import { readablePrivacyReport } from "../../../../utils/reporters"; import { BackendBonusVacanze } from "../../api/backendBonusVacanze"; import { loadAllBonusActivations, @@ -30,7 +30,7 @@ export function* handleLoadAllBonusActivations( `response status ${allBonusActivationsResponse.value.status}` ); } else { - throw Error(readableReport(allBonusActivationsResponse.value)); + throw Error(readablePrivacyReport(allBonusActivationsResponse.value)); } } catch (e) { yield put(loadAllBonusActivations.failure(e)); diff --git a/ts/features/bonusVacanze/store/sagas/handleLoadBonusVacanzeFromId.ts b/ts/features/bonusVacanze/store/sagas/handleLoadBonusVacanzeFromId.ts index 93c85a450de..19d89672c76 100644 --- a/ts/features/bonusVacanze/store/sagas/handleLoadBonusVacanzeFromId.ts +++ b/ts/features/bonusVacanze/store/sagas/handleLoadBonusVacanzeFromId.ts @@ -1,8 +1,8 @@ -import { readableReport } from "italia-ts-commons/lib/reporters"; import { SagaIterator } from "redux-saga"; import { call, put } from "redux-saga/effects"; import { ActionType } from "typesafe-actions"; import { SagaCallReturnType } from "../../../../types/utils"; +import { readablePrivacyReport } from "../../../../utils/reporters"; import { BackendBonusVacanze } from "../../api/backendBonusVacanze"; import { loadBonusVacanzeFromId } from "../actions/bonusVacanze"; @@ -26,7 +26,7 @@ export function* handleLoadBonusVacanzeFromId( } throw Error(`response status ${bonusVacanzeResponse.value.status}`); } else { - throw Error(readableReport(bonusVacanzeResponse.value)); + throw Error(readablePrivacyReport(bonusVacanzeResponse.value)); } } catch (e) { yield put(loadBonusVacanzeFromId.failure({ error: e, id: action.payload })); diff --git a/ts/utils/__tests__/messages.test.ts b/ts/utils/__tests__/messages.test.ts index c50599d353e..ac1b2addad7 100644 --- a/ts/utils/__tests__/messages.test.ts +++ b/ts/utils/__tests__/messages.test.ts @@ -67,7 +67,7 @@ describe("getCTA", () => { ); }); - it("should have 1 valid CTA", () => { + it("should not have valid CTA (action is malformed)", () => { const CTA_1 = `--- it: cta_1: @@ -86,25 +86,7 @@ some noise`; }, "it" ); - expect(maybeCTA.isSome()).toBeTruthy(); - if (maybeCTA.isSome()) { - const ctas = maybeCTA.value; - expect(ctas.cta_1).toBeDefined(); - expect(ctas.cta_2).not.toBeDefined(); - } - - const maybeCTAEn = getCTA( - { - ...messageWithContent, - content: { - ...messageWithContent.content, - markdown: CTA_1 as MessageBodyMarkdown - } - }, - "en" - ); - // this is because it fallbacks on the next locale supported (it in this case) - expect(maybeCTAEn.isSome()).toBeTruthy(); + expect(maybeCTA.isNone()); }); it("should not have a valid CTA", () => { diff --git a/ts/utils/messages.ts b/ts/utils/messages.ts index 2779a6bb9a0..15dd0aa9b64 100644 --- a/ts/utils/messages.ts +++ b/ts/utils/messages.ts @@ -25,6 +25,7 @@ import { deriveCustomHandledLink } from "../components/ui/Markdown/handlers/link import I18n, { translations } from "../i18n"; import { CTA, CTAS, MessageCTA } from "../types/MessageCTA"; import { getExpireStatus } from "./dates"; +import { getLocalePrimaryWithFallback } from "./locale"; import { isTextIncludedCaseInsensitive } from "./strings"; export function messageContainsText( @@ -189,7 +190,7 @@ export const getPrescriptionDataFromName = ( */ export const getCTA = ( message: CreatedMessageWithContent, - locale: Locales = I18n.currentLocale() + locale: Locales = getLocalePrimaryWithFallback(I18n.currentLocale()) ): Option => { return fromPredicate((t: string) => FM.test(t))(message.content.markdown) .map(m => FM(m).attributes) @@ -200,7 +201,12 @@ export const getCTA = ( s => attrs[s as Locales] !== undefined ); if (fallback) { - return CTAS.decode(attrs[fallback as Locales]).fold(__ => none, some); + // try decoding + return CTAS.decode(attrs[fallback as Locales]).fold( + __ => none, + // check the decode actions are valid + cta => (hasCtaValidActions(cta) ? some(cta) : none) + ); } return none; }, some) @@ -208,7 +214,7 @@ export const getCTA = ( }; /** - * return a Promise indicating if the cta action is valid or not + * return a boolean indicating if the cta action is valid or not * @param cta */ export const isCtaActionValid = (cta: CTA): boolean => { @@ -224,7 +230,11 @@ export const isCtaActionValid = (cta: CTA): boolean => { return false; }; -export const hasCTAValidActions = (ctas: CTAS): boolean => { +/** + * return true if at least one of the CTAs is valid + * @param ctas + */ +export const hasCtaValidActions = (ctas: CTAS): boolean => { const isCTA1Valid = isCtaActionValid(ctas.cta_1); if (ctas.cta_2 === undefined) { return isCTA1Valid;