diff --git a/ts/api/__mocks__/backend.ts b/ts/api/__mocks__/backend.ts index 1e9bbcacd79..06b35ce8526 100644 --- a/ts/api/__mocks__/backend.ts +++ b/ts/api/__mocks__/backend.ts @@ -1,9 +1,10 @@ /** * Mocked version of the BackendClient. */ - -const mockedGetSession = jest.fn(); - -export const BackendClient = () => ({ - getSession: mockedGetSession -}); +export const BackendClient = { + getMessage: jest.fn(), + getMessages: jest.fn(), + getSession: jest.fn(), + getThirdPartyMessagePrecondition: jest.fn(), + upsertMessageStatusAttributes: jest.fn() +}; diff --git a/ts/features/messages/saga/__test__/watchLoadMessageDetails.test.ts b/ts/features/messages/saga/__test__/handleLoadMessageDetails.test.ts similarity index 71% rename from ts/features/messages/saga/__test__/watchLoadMessageDetails.test.ts rename to ts/features/messages/saga/__test__/handleLoadMessageDetails.test.ts index 484bf5f19a9..5e76b277150 100644 --- a/ts/features/messages/saga/__test__/watchLoadMessageDetails.test.ts +++ b/ts/features/messages/saga/__test__/handleLoadMessageDetails.test.ts @@ -9,26 +9,28 @@ import { paymentValidInvalidAfterDueDate, successLoadMessageDetails } from "../../__mocks__/message"; -import { testTryLoadMessageDetails } from "../watchLoadMessageDetails"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; - -const tryLoadMessageDetails = testTryLoadMessageDetails!; +import { handleLoadMessageDetails } from "../handleLoadMessageDetails"; +import { BackendClient } from "../../../../api/__mocks__/backend"; const id = paymentValidInvalidAfterDueDate.id as UIMessageId; -describe("tryReloadAllMessages", () => { +describe("handleLoadMessageDetails", () => { const getMessagesPayload = { id }; describe("when the response is successful", () => { it(`should put ${getType( action.success )} with the parsed messages and pagination data`, () => { - const getMessage = jest.fn(); - testSaga(tryLoadMessageDetails(getMessage), action.request({ id })) + testSaga( + handleLoadMessageDetails, + BackendClient.getMessage, + action.request({ id }) + ) .next() .call( withRefreshApiCall, - getMessage(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), action.request({ id }) ) // .call(getMessage, getMessagesPayload) @@ -41,12 +43,15 @@ describe("tryReloadAllMessages", () => { describe("when the response is an Error", () => { it(`should put ${getType(action.failure)} with the error message`, () => { - const getMessage = jest.fn(); - testSaga(tryLoadMessageDetails(getMessage), action.request({ id })) + testSaga( + handleLoadMessageDetails, + BackendClient.getMessage, + action.request({ id }) + ) .next() .call( withRefreshApiCall, - getMessage(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), action.request({ id }) ) .next(E.right({ status: 500, value: { title: "Backend error" } })) @@ -58,12 +63,13 @@ describe("tryReloadAllMessages", () => { describe("when the handler throws", () => { it(`should catch it and put ${getType(action.failure)}`, () => { - const getMessage = jest.fn().mockImplementation(() => { - throw new Error("I made a boo-boo, sir!"); - }); - - testSaga(tryLoadMessageDetails(getMessage), action.request({ id })) + testSaga( + handleLoadMessageDetails, + BackendClient.getMessage, + action.request({ id }) + ) .next() + .throw(new Error("I made a boo-boo, sir!")) .put( action.failure({ id, diff --git a/ts/features/messages/saga/__test__/watchLoadNextPageMessages.test.ts b/ts/features/messages/saga/__test__/handleLoadNextPageMessages.test.ts similarity index 71% rename from ts/features/messages/saga/__test__/watchLoadNextPageMessages.test.ts rename to ts/features/messages/saga/__test__/handleLoadNextPageMessages.test.ts index 6a58281e9f0..4052fe2c0a1 100644 --- a/ts/features/messages/saga/__test__/watchLoadNextPageMessages.test.ts +++ b/ts/features/messages/saga/__test__/handleLoadNextPageMessages.test.ts @@ -2,21 +2,17 @@ import * as E from "fp-ts/lib/Either"; import { testSaga } from "redux-saga-test-plan"; import { getType } from "typesafe-actions"; -import { - loadNextPageMessages as action, - loadNextPageMessages -} from "../../store/actions"; +import { loadNextPageMessages as action } from "../../store/actions"; import { apiPayload, defaultRequestPayload, successLoadNextPageMessagesPayload } from "../../__mocks__/messages"; -import { testTryLoadNextPageMessages } from "../watchLoadNextPageMessages"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { handleLoadNextPageMessages } from "../handleLoadNextPageMessages"; +import { BackendClient } from "../../../../api/__mocks__/backend"; -const tryLoadNextPageMessages = testTryLoadNextPageMessages!; - -describe("tryLoadNextPageMessages", () => { +describe("handleLoadNextPageMessages", () => { const getMessagesPayload = { enrich_result_data: true, page_size: defaultRequestPayload.pageSize, @@ -28,16 +24,16 @@ describe("tryLoadNextPageMessages", () => { it(`should put ${getType( action.success )} with the parsed messages and pagination data`, () => { - const getMessages = jest.fn(); testSaga( - tryLoadNextPageMessages(getMessages), + handleLoadNextPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), - loadNextPageMessages.request(defaultRequestPayload) + BackendClient.getMessages(getMessagesPayload), + action.request(defaultRequestPayload) ) .next(E.right({ status: 200, value: apiPayload })) .put(action.success(successLoadNextPageMessagesPayload)) @@ -48,16 +44,16 @@ describe("tryLoadNextPageMessages", () => { describe("when the response is an Error", () => { it(`should put ${getType(action.failure)} with the error message`, () => { - const getMessages = jest.fn(); testSaga( - tryLoadNextPageMessages(getMessages), + handleLoadNextPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), - loadNextPageMessages.request(defaultRequestPayload) + BackendClient.getMessages(getMessagesPayload), + action.request(defaultRequestPayload) ) .next(E.right({ status: 500, value: { title: "Backend error" } })) .put( @@ -73,14 +69,13 @@ describe("tryLoadNextPageMessages", () => { describe("when the handler throws", () => { it(`should catch it and put ${getType(action.failure)}`, () => { - const getMessages = () => { - throw new Error("I made a boo-boo, sir!"); - }; testSaga( - tryLoadNextPageMessages(getMessages), + handleLoadNextPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() + .throw(new Error("I made a boo-boo, sir!")) .put( action.failure({ error: new Error("I made a boo-boo, sir!"), diff --git a/ts/features/messages/saga/__test__/watchLoadPreviousPageMessages.test.ts b/ts/features/messages/saga/__test__/handleLoadPreviousPageMessages.test.ts similarity index 79% rename from ts/features/messages/saga/__test__/watchLoadPreviousPageMessages.test.ts rename to ts/features/messages/saga/__test__/handleLoadPreviousPageMessages.test.ts index 2c392d944ec..c85558c1ae2 100644 --- a/ts/features/messages/saga/__test__/watchLoadPreviousPageMessages.test.ts +++ b/ts/features/messages/saga/__test__/handleLoadPreviousPageMessages.test.ts @@ -11,12 +11,11 @@ import { defaultRequestPayload, successLoadPreviousPageMessagesPayload } from "../../__mocks__/messages"; -import { testTryLoadPreviousPageMessages } from "../watchLoadPreviousPageMessages"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { handleLoadPreviousPageMessages } from "../handleLoadPreviousPageMessages"; +import { BackendClient } from "../../../../api/__mocks__/backend"; -const tryLoadPreviousPageMessages = testTryLoadPreviousPageMessages!; - -describe("tryLoadPreviousPageMessages", () => { +describe("handleLoadPreviousPageMessages", () => { const getMessagesPayload = { enrich_result_data: true, page_size: 8, @@ -28,15 +27,15 @@ describe("tryLoadPreviousPageMessages", () => { it(`should put ${getType( action.success )} with the parsed messages and pagination data`, () => { - const getMessages = jest.fn(); testSaga( - tryLoadPreviousPageMessages(getMessages), + handleLoadPreviousPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), loadPreviousPageMessages.request(defaultRequestPayload) ) .next(E.right({ status: 200, value: apiPayload })) @@ -48,15 +47,15 @@ describe("tryLoadPreviousPageMessages", () => { describe("when the response is an Error", () => { it(`should put ${getType(action.failure)} with the error message`, () => { - const getMessages = jest.fn(); testSaga( - tryLoadPreviousPageMessages(getMessages), + handleLoadPreviousPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), loadPreviousPageMessages.request(defaultRequestPayload) ) .next(E.right({ status: 500, value: { title: "Backend error" } })) @@ -73,14 +72,13 @@ describe("tryLoadPreviousPageMessages", () => { describe("when the handler throws", () => { it(`should catch it and put ${getType(action.failure)}`, () => { - const getMessages = () => { - throw new Error("I made a boo-boo, sir!"); - }; testSaga( - tryLoadPreviousPageMessages(getMessages), + handleLoadPreviousPageMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() + .throw(new Error("I made a boo-boo, sir!")) .put( action.failure({ error: new Error("I made a boo-boo, sir!"), diff --git a/ts/features/messages/saga/__test__/watchMessagePrecondition.test.ts b/ts/features/messages/saga/__test__/handleMessagePrecondition.test.ts similarity index 74% rename from ts/features/messages/saga/__test__/watchMessagePrecondition.test.ts rename to ts/features/messages/saga/__test__/handleMessagePrecondition.test.ts index b8732906de2..47e4cb8e464 100644 --- a/ts/features/messages/saga/__test__/watchMessagePrecondition.test.ts +++ b/ts/features/messages/saga/__test__/handleMessagePrecondition.test.ts @@ -3,12 +3,13 @@ import { testSaga } from "redux-saga-test-plan"; import { getType } from "typesafe-actions"; import { getMessagePrecondition } from "../../store/actions"; import { UIMessageId } from "../../types"; -import { testWorkerMessagePrecondition } from "../watchMessagePrecondition"; +import { testMessagePreconditionWorker } from "../handleMessagePrecondition"; import { ThirdPartyMessagePrecondition } from "../../../../../definitions/backend/ThirdPartyMessagePrecondition"; import { TagEnum as TagEnumPN } from "../../../../../definitions/backend/MessageCategoryPN"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { BackendClient } from "../../../../api/__mocks__/backend"; -const workerMessagePrecondition = testWorkerMessagePrecondition!; +const messagePreconditionWorker = testMessagePreconditionWorker!; const action = { id: "MSG001" as UIMessageId, @@ -19,21 +20,19 @@ const mockResponseSuccess: ThirdPartyMessagePrecondition = { markdown: "-" }; -describe("workerMessagePrecondition", () => { +describe("messagePreconditionWorker", () => { it(`should put ${getType( getMessagePrecondition.success )} when the response is successful`, () => { - const getThirdPartyMessagePrecondition = jest.fn(); - testSaga( - workerMessagePrecondition, - getThirdPartyMessagePrecondition, + messagePreconditionWorker, + BackendClient.getThirdPartyMessagePrecondition, getMessagePrecondition.request(action) ) .next() .call( withRefreshApiCall, - getThirdPartyMessagePrecondition(action), + BackendClient.getThirdPartyMessagePrecondition(action), getMessagePrecondition.request(action) ) .next(E.right({ status: 200, value: mockResponseSuccess })) @@ -45,17 +44,15 @@ describe("workerMessagePrecondition", () => { it(`should put ${getType( getMessagePrecondition.failure )} when the response is an error`, () => { - const getThirdPartyMessagePrecondition = jest.fn(); - testSaga( - workerMessagePrecondition, - getThirdPartyMessagePrecondition, + messagePreconditionWorker, + BackendClient.getThirdPartyMessagePrecondition, getMessagePrecondition.request(action) ) .next() .call( withRefreshApiCall, - getThirdPartyMessagePrecondition(action), + BackendClient.getThirdPartyMessagePrecondition(action), getMessagePrecondition.request(action) ) .next(E.right({ status: 500, value: `response status ${500}` })) @@ -67,11 +64,9 @@ describe("workerMessagePrecondition", () => { it(`should put ${getType( getMessagePrecondition.failure )} when the handler throws an exception`, () => { - const getThirdPartyMessagePrecondition = jest.fn(); - testSaga( - workerMessagePrecondition, - getThirdPartyMessagePrecondition, + messagePreconditionWorker, + BackendClient.getThirdPartyMessagePrecondition, getMessagePrecondition.request(action) ) .next() diff --git a/ts/features/messages/saga/__test__/watchReloadAllMessages.test.ts b/ts/features/messages/saga/__test__/handleReloadAllMessages.test.ts similarity index 78% rename from ts/features/messages/saga/__test__/watchReloadAllMessages.test.ts rename to ts/features/messages/saga/__test__/handleReloadAllMessages.test.ts index 3380428b86b..6399fffc960 100644 --- a/ts/features/messages/saga/__test__/watchReloadAllMessages.test.ts +++ b/ts/features/messages/saga/__test__/handleReloadAllMessages.test.ts @@ -9,12 +9,11 @@ import { defaultRequestPayload, successReloadMessagesPayload } from "../../__mocks__/messages"; -import { testTryLoadPreviousPageMessages } from "../watchReloadAllMessages"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { handleReloadAllMessages } from "../handleReloadAllMessages"; +import { BackendClient } from "../../../../api/__mocks__/backend"; -const tryReloadAllMessages = testTryLoadPreviousPageMessages!; - -describe("tryReloadAllMessages", () => { +describe("handleReloadAllMessages", () => { const getMessagesPayload = { enrich_result_data: true, page_size: defaultRequestPayload.pageSize, @@ -25,15 +24,15 @@ describe("tryReloadAllMessages", () => { it(`should put ${getType( action.success )} with the parsed messages and pagination data`, () => { - const getMessages = jest.fn(); testSaga( - tryReloadAllMessages(getMessages), + handleReloadAllMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), action.request(defaultRequestPayload) ) .next(E.right({ status: 200, value: apiPayload })) @@ -45,15 +44,15 @@ describe("tryReloadAllMessages", () => { describe("when the response is an Error", () => { it(`should put ${getType(action.failure)} with the error message`, () => { - const getMessages = jest.fn(); testSaga( - tryReloadAllMessages(getMessages), + handleReloadAllMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() .call( withRefreshApiCall, - getMessages(getMessagesPayload), + BackendClient.getMessages(getMessagesPayload), action.request(defaultRequestPayload) ) .next( @@ -70,14 +69,13 @@ describe("tryReloadAllMessages", () => { describe("when the handler throws", () => { it(`should catch it and put ${getType(action.failure)}`, () => { - const getMessages = () => { - throw new Error(defaultRequestError.error.message); - }; testSaga( - tryReloadAllMessages(getMessages), + handleReloadAllMessages, + BackendClient.getMessages, action.request(defaultRequestPayload) ) .next() + .throw(new Error(defaultRequestError.error.message)) .put( action.failure({ error: new Error(defaultRequestError.error.message), diff --git a/ts/features/messages/saga/__test__/watchUpsertMessageStatusAttributes.test.ts b/ts/features/messages/saga/__test__/handleUpsertMessageStatusAttribues.test.ts similarity index 76% rename from ts/features/messages/saga/__test__/watchUpsertMessageStatusAttributes.test.ts rename to ts/features/messages/saga/__test__/handleUpsertMessageStatusAttribues.test.ts index be2b59d94ef..e784cdfddb0 100644 --- a/ts/features/messages/saga/__test__/watchUpsertMessageStatusAttributes.test.ts +++ b/ts/features/messages/saga/__test__/handleUpsertMessageStatusAttribues.test.ts @@ -1,19 +1,17 @@ import * as E from "fp-ts/lib/Either"; import { testSaga } from "redux-saga-test-plan"; import { getType } from "typesafe-actions"; - import { upsertMessageStatusAttributes as action, UpsertMessageStatusAttributesPayload } from "../../store/actions"; import { UIMessageId } from "../../types"; import { successReloadMessagesPayload } from "../../__mocks__/messages"; -import { testTryUpsertMessageStatusAttributes } from "../watchUpsertMessageStatusAttribues"; import { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { handleUpsertMessageStatusAttribues } from "../handleUpsertMessageStatusAttribues"; +import { BackendClient } from "../../../../api/__mocks__/backend"; -const tryUpsertMessageStatusAttributes = testTryUpsertMessageStatusAttributes!; - -describe("tryUpsertMessageStatusAttributes", () => { +describe("handleUpsertMessageStatusAttribues", () => { const actionPayload: UpsertMessageStatusAttributesPayload = { message: { ...successReloadMessagesPayload.messages[0], @@ -35,15 +33,15 @@ describe("tryUpsertMessageStatusAttributes", () => { it(`should put ${getType( action.success )} with the original payload`, () => { - const putMessage = jest.fn(); testSaga( - tryUpsertMessageStatusAttributes(putMessage), + handleUpsertMessageStatusAttribues, + BackendClient.upsertMessageStatusAttributes, action.request(actionPayload) ) .next() .call( withRefreshApiCall, - putMessage(callPayload), + BackendClient.upsertMessageStatusAttributes(callPayload), action.request(actionPayload) ) .next(E.right({ status: 200, value: {} })) @@ -55,15 +53,15 @@ describe("tryUpsertMessageStatusAttributes", () => { describe("when the response is an Error", () => { it(`should put ${getType(action.failure)} with the error message`, () => { - const putMessage = jest.fn(); testSaga( - tryUpsertMessageStatusAttributes(putMessage), + handleUpsertMessageStatusAttribues, + BackendClient.upsertMessageStatusAttributes, action.request(actionPayload) ) .next() .call( withRefreshApiCall, - putMessage(callPayload), + BackendClient.upsertMessageStatusAttributes(callPayload), action.request(actionPayload) ) .next( @@ -85,14 +83,13 @@ describe("tryUpsertMessageStatusAttributes", () => { describe("when the handler throws", () => { it(`should catch it and put ${getType(action.failure)}`, () => { - const putMessage = () => { - throw new Error("462e5dffdb46435995d545999bed6b11"); - }; testSaga( - tryUpsertMessageStatusAttributes(putMessage), + handleUpsertMessageStatusAttribues, + BackendClient.upsertMessageStatusAttributes, action.request(actionPayload) ) .next() + .throw(new Error("462e5dffdb46435995d545999bed6b11")) .put( action.failure({ error: new Error("462e5dffdb46435995d545999bed6b11"), diff --git a/ts/features/messages/saga/watchLoadMessageById.ts b/ts/features/messages/saga/handleLoadMessageById.ts similarity index 75% rename from ts/features/messages/saga/watchLoadMessageById.ts rename to ts/features/messages/saga/handleLoadMessageById.ts index 839599d5647..505a16ba8e2 100644 --- a/ts/features/messages/saga/watchLoadMessageById.ts +++ b/ts/features/messages/saga/handleLoadMessageById.ts @@ -1,6 +1,5 @@ -import { takeEvery, put, call } from "typed-redux-saga/macro"; +import { call, put } from "typed-redux-saga/macro"; import { ActionType } from "typesafe-actions"; -import { SagaIterator } from "redux-saga"; import { convertUnknownToError } from "../../../utils/errors"; import { BackendClient } from "../../../api/backend"; import { loadMessageById } from "../store/actions"; @@ -12,17 +11,10 @@ import { errorToReason, unknownToReason } from "../utils"; import { trackLoadMessageByIdFailure } from "../analytics"; import { handleResponse } from "../utils/responseHandling"; -type LocalActionType = ActionType<(typeof loadMessageById)["request"]>; -type LocalBeClient = ReturnType["getMessage"]; - -export function* watchLoadMessageById(getMessage: LocalBeClient): SagaIterator { - yield* takeEvery(loadMessageById.request, handleLoadMessageById, getMessage); -} - -function* handleLoadMessageById( - getMessage: LocalBeClient, - action: LocalActionType -): SagaIterator { +export function* handleLoadMessageById( + getMessage: BackendClient["getMessage"], + action: ActionType +) { const id = action.payload.id; try { diff --git a/ts/features/messages/saga/handleLoadMessageDetails.ts b/ts/features/messages/saga/handleLoadMessageDetails.ts new file mode 100644 index 00000000000..81c33ffe32d --- /dev/null +++ b/ts/features/messages/saga/handleLoadMessageDetails.ts @@ -0,0 +1,53 @@ +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { CreatedMessageWithContentAndAttachments } from "../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; +import { BackendClient } from "../../../api/backend"; +import { loadMessageDetails } from "../store/actions"; +import { SagaCallReturnType } from "../../../types/utils"; +import { getError } from "../../../utils/errors"; +import { toUIMessageDetails } from "../store/reducers/transformers"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { errorToReason, unknownToReason } from "../utils"; +import { trackLoadMessageDetailsFailure } from "../analytics"; +import { handleResponse } from "../utils/responseHandling"; + +export function* handleLoadMessageDetails( + getMessage: BackendClient["getMessage"], + action: ActionType +) { + const id = action.payload.id; + + try { + const response = (yield* call( + withRefreshApiCall, + getMessage({ id }), + action + )) as unknown as SagaCallReturnType; + const nextAction = handleResponse( + response, + (message: CreatedMessageWithContentAndAttachments) => + loadMessageDetails.success(toUIMessageDetails(message)), + error => { + const reason = errorToReason(error); + trackLoadMessageDetailsFailure(reason); + return loadMessageDetails.failure({ + id, + error: getError(error) + }); + } + ); + + if (nextAction) { + yield* put(nextAction); + } + } catch (error) { + const reason = unknownToReason(error); + trackLoadMessageDetailsFailure(reason); + yield* put( + loadMessageDetails.failure({ + id, + error: getError(error) + }) + ); + } +} diff --git a/ts/features/messages/saga/handleLoadNextPageMessages.ts b/ts/features/messages/saga/handleLoadNextPageMessages.ts new file mode 100644 index 00000000000..e95b701d917 --- /dev/null +++ b/ts/features/messages/saga/handleLoadNextPageMessages.ts @@ -0,0 +1,65 @@ +import { put, call } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; +import { BackendClient } from "../../../api/backend"; +import { + loadNextPageMessages, + loadNextPageMessages as loadNextPageMessagesAction +} from "../store/actions"; +import { toUIMessage } from "../store/reducers/transformers"; +import { SagaCallReturnType } from "../../../types/utils"; +import { convertUnknownToError, getError } from "../../../utils/errors"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { errorToReason, unknownToReason } from "../utils"; +import { trackLoadNextPageMessagesFailure } from "../analytics"; +import { handleResponse } from "../utils/responseHandling"; + +export function* handleLoadNextPageMessages( + getMessages: BackendClient["getMessages"], + action: ActionType +) { + const { filter, pageSize, cursor } = action.payload; + + try { + const response = (yield* call( + withRefreshApiCall, + getMessages({ + enrich_result_data: true, + page_size: pageSize, + maximum_id: cursor, + archived: filter.getArchived + }), + action + )) as unknown as SagaCallReturnType; + const nextAction = handleResponse( + response, + ({ items, next }: PaginatedPublicMessagesCollection) => + loadNextPageMessagesAction.success({ + messages: items.map(toUIMessage), + pagination: { next }, + filter + }), + error => { + const reason = errorToReason(error); + trackLoadNextPageMessagesFailure(reason); + return loadNextPageMessagesAction.failure({ + error: getError(error), + filter + }); + } + ); + + if (nextAction) { + yield* put(nextAction); + } + } catch (e) { + const reason = unknownToReason(e); + trackLoadNextPageMessagesFailure(reason); + yield* put( + loadNextPageMessagesAction.failure({ + error: convertUnknownToError(e), + filter + }) + ); + } +} diff --git a/ts/features/messages/saga/handleLoadPreviousPageMessages.ts b/ts/features/messages/saga/handleLoadPreviousPageMessages.ts new file mode 100644 index 00000000000..d10f8aa5f95 --- /dev/null +++ b/ts/features/messages/saga/handleLoadPreviousPageMessages.ts @@ -0,0 +1,62 @@ +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { BackendClient } from "../../../api/backend"; +import { loadPreviousPageMessages as loadPreviousPageMessagesAction } from "../store/actions"; +import { SagaCallReturnType } from "../../../types/utils"; +import { toUIMessage } from "../store/reducers/transformers"; +import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; +import { convertUnknownToError, getError } from "../../../utils/errors"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { errorToReason, unknownToReason } from "../utils"; +import { trackLoadPreviousPageMessagesFailure } from "../analytics"; +import { handleResponse } from "../utils/responseHandling"; + +export function* handleLoadPreviousPageMessages( + getMessages: BackendClient["getMessages"], + action: ActionType +) { + const { filter, cursor, pageSize } = action.payload; + + try { + const response = (yield* call( + withRefreshApiCall, + getMessages({ + enrich_result_data: true, + page_size: pageSize, + minimum_id: cursor, + archived: filter.getArchived + }), + action + )) as unknown as SagaCallReturnType; + const nextAction = handleResponse( + response, + ({ items, prev }: PaginatedPublicMessagesCollection) => + loadPreviousPageMessagesAction.success({ + messages: items.map(toUIMessage), + pagination: { previous: prev }, + filter + }), + error => { + const reason = errorToReason(error); + trackLoadPreviousPageMessagesFailure(reason); + return loadPreviousPageMessagesAction.failure({ + error: getError(error), + filter + }); + } + ); + + if (nextAction) { + yield* put(nextAction); + } + } catch (e) { + const reason = unknownToReason(e); + trackLoadPreviousPageMessagesFailure(reason); + yield* put( + loadPreviousPageMessagesAction.failure({ + error: convertUnknownToError(e), + filter + }) + ); + } +} diff --git a/ts/features/messages/saga/watchMessagePrecondition.ts b/ts/features/messages/saga/handleMessagePrecondition.ts similarity index 66% rename from ts/features/messages/saga/watchMessagePrecondition.ts rename to ts/features/messages/saga/handleMessagePrecondition.ts index 32ded43a2fa..3110b5c65f2 100644 --- a/ts/features/messages/saga/watchMessagePrecondition.ts +++ b/ts/features/messages/saga/handleMessagePrecondition.ts @@ -1,8 +1,7 @@ import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import * as E from "fp-ts/lib/Either"; -import { SagaIterator } from "redux-saga"; -import { call, put, race, take, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; +import { call, put, race, take } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; import { BackendClient } from "../../../api/backend"; import { convertUnknownToError } from "../../../utils/errors"; import { @@ -14,11 +13,11 @@ import { withRefreshApiCall } from "../../fastLogin/saga/utils"; import { SagaCallReturnType } from "../../../types/utils"; import { trackDisclaimerLoadError } from "../analytics"; -export const testWorkerMessagePrecondition = isTestEnv - ? workerMessagePrecondition +export const testMessagePreconditionWorker = isTestEnv + ? messagePreconditionWorker : undefined; -function* workerMessagePrecondition( +function* messagePreconditionWorker( getThirdPartyMessagePrecondition: ReturnType< BackendClient["getThirdPartyMessagePrecondition"] >, @@ -50,20 +49,16 @@ function* workerMessagePrecondition( } } -export function* watchMessagePrecondition( - getThirdPartyMessagePrecondition: BackendClient["getThirdPartyMessagePrecondition"] -): SagaIterator { - yield* takeLatest( - getType(getMessagePrecondition.request), - function* (action: ActionType) { - yield* race({ - response: call( - workerMessagePrecondition, - getThirdPartyMessagePrecondition(), - action - ), - cancel: take(clearMessagePrecondition) - }); - } - ); +export function* handleMessagePrecondition( + getThirdPartyMessagePrecondition: BackendClient["getThirdPartyMessagePrecondition"], + action: ActionType +) { + yield* race({ + response: call( + messagePreconditionWorker, + getThirdPartyMessagePrecondition(), + action + ), + cancel: take(clearMessagePrecondition) + }); } diff --git a/ts/features/messages/saga/handleMigrateToPagination.ts b/ts/features/messages/saga/handleMigrateToPagination.ts new file mode 100644 index 00000000000..28d890f6683 --- /dev/null +++ b/ts/features/messages/saga/handleMigrateToPagination.ts @@ -0,0 +1,95 @@ +import { ValidationError } from "io-ts"; +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { MessageStatusArchivingChange } from "../../../../definitions/backend/MessageStatusArchivingChange"; +import { MessageStatusBulkChange } from "../../../../definitions/backend/MessageStatusBulkChange"; +import { BackendClient } from "../../../api/backend"; +import migrateToPagination from "../utils/migrateToPagination"; +import { migrateToPaginatedMessages, removeMessages } from "../store/actions"; +import { MessageStatus } from "../store/reducers/messagesStatus"; +import { readablePrivacyReport } from "../../../utils/reporters"; + +export function* handleMigrateToPagination( + putMessages: BackendClient["upsertMessageStatusAttributes"], + action: ActionType +) { + try { + const { bogus, toMigrate } = Object.keys(action.payload).reduce<{ + bogus: Array; + toMigrate: Array<{ id: string; isArchived: boolean; isRead: boolean }>; + }>( + (acc, id) => { + const status = action.payload[id]; + if (status && (status.isRead || status.isArchived)) { + return { + ...acc, + toMigrate: [ + ...acc.toMigrate, + { + id, + isRead: status.isRead, + isArchived: status.isArchived + } as { + id: string; + isArchived: boolean; + isRead: boolean; + } + ] + }; + } + return { ...acc, bogus: [...acc.bogus, id] }; + }, + { bogus: [], toMigrate: [] } + ); + + if (toMigrate.length === 0) { + yield* put(removeMessages(bogus)); + yield* put(migrateToPaginatedMessages.success(0)); + return; + } + + const { failed, succeeded } = yield* call( + migrateToPagination, + toMigrate, + (id: string, { isRead, isArchived }: MessageStatus) => { + if (isRead) { + return putMessages({ + id, + body: { + change_type: "bulk", + is_read: true, + is_archived: isArchived + } as MessageStatusBulkChange + }); + } + return putMessages({ + id, + body: { + change_type: "archiving", + is_archived: isArchived + } as MessageStatusArchivingChange + }); + } + ); + + yield* put(removeMessages(succeeded.concat(bogus))); + + if (failed.length === 0) { + yield* put(migrateToPaginatedMessages.success(succeeded.length)); + } else { + yield* put(migrateToPaginatedMessages.failure({ succeeded, failed })); + } + } catch (e) { + // assuming the worst, no messages were migrated because of an unexpected failure + const errorPayload = { + succeeded: [], + failed: Object.keys(action.payload).map(id => ({ + messageId: id, + + // FIXME: This is potentially unsafe. + error: readablePrivacyReport(e as Array) + })) + }; + yield* put(migrateToPaginatedMessages.failure(errorPayload)); + } +} diff --git a/ts/features/messages/saga/handleReloadAllMessages.ts b/ts/features/messages/saga/handleReloadAllMessages.ts new file mode 100644 index 00000000000..d82dcb24977 --- /dev/null +++ b/ts/features/messages/saga/handleReloadAllMessages.ts @@ -0,0 +1,64 @@ +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { BackendClient } from "../../../api/backend"; +import { + reloadAllMessages, + reloadAllMessages as reloadAllMessagesAction +} from "../store/actions"; +import { SagaCallReturnType } from "../../../types/utils"; +import { toUIMessage } from "../store/reducers/transformers"; +import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; +import { getError } from "../../../utils/errors"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { errorToReason, unknownToReason } from "../utils"; +import { trackReloadAllMessagesFailure } from "../analytics"; +import { handleResponse } from "../utils/responseHandling"; + +export function* handleReloadAllMessages( + getMessages: BackendClient["getMessages"], + action: ActionType +) { + const { filter, pageSize } = action.payload; + + try { + const response: SagaCallReturnType = (yield* call( + withRefreshApiCall, + getMessages({ + enrich_result_data: true, + page_size: pageSize, + archived: filter.getArchived + }), + action + )) as unknown as SagaCallReturnType; + const nextAction = handleResponse( + response, + ({ items, next, prev }: PaginatedPublicMessagesCollection) => + reloadAllMessagesAction.success({ + messages: items.map(toUIMessage), + pagination: { previous: prev, next }, + filter + }), + error => { + const reason = errorToReason(error); + trackReloadAllMessagesFailure(reason); + return reloadAllMessagesAction.failure({ + error: getError(error), + filter + }); + } + ); + + if (nextAction) { + yield* put(nextAction); + } + } catch (error) { + const reason = unknownToReason(error); + trackReloadAllMessagesFailure(reason); + yield* put( + reloadAllMessagesAction.failure({ + error: getError(error), + filter + }) + ); + } +} diff --git a/ts/features/messages/saga/watchThirdPartyMessageSaga.ts b/ts/features/messages/saga/handleThirdPartyMessage.ts similarity index 84% rename from ts/features/messages/saga/watchThirdPartyMessageSaga.ts rename to ts/features/messages/saga/handleThirdPartyMessage.ts index 6b81aace18a..ff7a0f1f699 100644 --- a/ts/features/messages/saga/watchThirdPartyMessageSaga.ts +++ b/ts/features/messages/saga/handleThirdPartyMessage.ts @@ -1,9 +1,8 @@ import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import * as E from "fp-ts/lib/Either"; import * as O from "fp-ts/lib/Option"; -import { SagaIterator } from "redux-saga"; -import { put, takeLatest, call } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; import { BackendClient } from "../../../api/backend"; import { loadThirdPartyMessage } from "../store/actions"; import { toPNMessage } from "../../pn/store/types/transformers"; @@ -24,31 +23,50 @@ import { ThirdPartyMessageWithContent } from "../../../../definitions/backend/Th import { ServiceId } from "../../../../definitions/backend/ServiceId"; import { TagEnum } from "../../../../definitions/backend/MessageCategoryPN"; -export function* watchThirdPartyMessageSaga( - client: BackendClient -): SagaIterator { - yield* takeLatest( - getType(loadThirdPartyMessage.request), - getThirdPartyMessage, - client - ); -} +const trackSuccess = ( + messageFromApi: ThirdPartyMessageWithContent, + tag: string +) => { + trackRemoteContentLoadSuccess(tag); + if (tag === TagEnum.PN) { + const pnMessageOption = toPNMessage(messageFromApi); + + if (O.isSome(pnMessageOption)) { + const pnMessage = pnMessageOption.value; + trackPNNotificationLoadSuccess(pnMessage); + } else { + trackPNNotificationLoadError(); + } + } else { + const attachments = messageFromApi.third_party_message.attachments; + const attachmentCount = attachments?.length ?? 0; + trackThirdPartyMessageAttachmentCount(attachmentCount); + } +}; + +const trackFailure = (reason: string, serviceId: ServiceId, tag: string) => { + trackRemoteContentLoadFailure(serviceId, tag, reason); + + if (tag === TagEnum.PN) { + trackPNNotificationLoadError(reason); + } +}; -function* getThirdPartyMessage( - client: BackendClient, +export function* handleThirdPartyMessage( + getThirdPartyMessage: BackendClient["getThirdPartyMessage"], action: ActionType ) { const { id, serviceId, tag } = action.payload; trackRemoteContentLoadRequest(tag); - const getThirdPartyMessage = client.getThirdPartyMessage(); + const getThirdPartyMessageRequest = getThirdPartyMessage(); try { const result = (yield* call( withRefreshApiCall, - getThirdPartyMessage({ id }), + getThirdPartyMessageRequest({ id }), action - )) as unknown as SagaCallReturnType; + )) as unknown as SagaCallReturnType; if (E.isLeft(result)) { const reason = readableReport(result.left); throw new Error(reason); @@ -70,32 +88,3 @@ function* getThirdPartyMessage( yield* put(loadThirdPartyMessage.failure({ id, error: new Error(reason) })); } } - -const trackSuccess = ( - messageFromApi: ThirdPartyMessageWithContent, - tag: string -) => { - trackRemoteContentLoadSuccess(tag); - if (tag === TagEnum.PN) { - const pnMessageOption = toPNMessage(messageFromApi); - - if (O.isSome(pnMessageOption)) { - const pnMessage = pnMessageOption.value; - trackPNNotificationLoadSuccess(pnMessage); - } else { - trackPNNotificationLoadError(); - } - } else { - const attachments = messageFromApi.third_party_message.attachments; - const attachmentCount = attachments?.length ?? 0; - trackThirdPartyMessageAttachmentCount(attachmentCount); - } -}; - -const trackFailure = (reason: string, serviceId: ServiceId, tag: string) => { - trackRemoteContentLoadFailure(serviceId, tag, reason); - - if (tag === TagEnum.PN) { - trackPNNotificationLoadError(reason); - } -}; diff --git a/ts/features/messages/saga/handleUpsertMessageStatusAttribues.ts b/ts/features/messages/saga/handleUpsertMessageStatusAttribues.ts new file mode 100644 index 00000000000..73ab778761d --- /dev/null +++ b/ts/features/messages/saga/handleUpsertMessageStatusAttribues.ts @@ -0,0 +1,86 @@ +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { MessageStatusArchivingChange } from "../../../../definitions/backend/MessageStatusArchivingChange"; +import { MessageStatusBulkChange } from "../../../../definitions/backend/MessageStatusBulkChange"; +import { MessageStatusChange } from "../../../../definitions/backend/MessageStatusChange"; +import { MessageStatusReadingChange } from "../../../../definitions/backend/MessageStatusReadingChange"; +import { BackendClient } from "../../../api/backend"; +import { + upsertMessageStatusAttributes, + UpsertMessageStatusAttributesPayload +} from "../store/actions"; +import { SagaCallReturnType } from "../../../types/utils"; +import { getError } from "../../../utils/errors"; +import { withRefreshApiCall } from "../../fastLogin/saga/utils"; +import { errorToReason, unknownToReason } from "../utils"; +import { trackUpsertMessageStatusAttributesFailure } from "../analytics"; +import { handleResponse } from "../utils/responseHandling"; + +/** + * @throws invalid payload + * @param payload + */ +function validatePayload( + payload: UpsertMessageStatusAttributesPayload +): MessageStatusChange { + switch (payload.update.tag) { + case "archiving": + return { + change_type: "archiving", + is_archived: payload.update.isArchived + } as MessageStatusArchivingChange; + case "reading": + return { + change_type: "reading", + is_read: true + } as MessageStatusReadingChange; + case "bulk": + return { + change_type: "bulk", + is_read: true, + is_archived: payload.update.isArchived + } as MessageStatusBulkChange; + default: + throw new TypeError("invalid payload"); + } +} + +export function* handleUpsertMessageStatusAttribues( + putMessage: BackendClient["upsertMessageStatusAttributes"], + action: ActionType +) { + try { + const body = validatePayload(action.payload); + const response = (yield* call( + withRefreshApiCall, + putMessage({ id: action.payload.message.id, body }), + action + )) as unknown as SagaCallReturnType; + + const nextAction = handleResponse( + response, + _ => upsertMessageStatusAttributes.success(action.payload), + error => { + const reason = errorToReason(error); + trackUpsertMessageStatusAttributesFailure(reason); + return upsertMessageStatusAttributes.failure({ + error: getError(error), + payload: action.payload + }); + } + ); + + if (nextAction) { + yield* put(nextAction); + } + } catch (error) { + const reason = unknownToReason(error); + trackUpsertMessageStatusAttributesFailure(reason); + yield* put( + upsertMessageStatusAttributes.failure({ + error: getError(error), + payload: action.payload + }) + ); + } +} diff --git a/ts/features/messages/saga/index.ts b/ts/features/messages/saga/index.ts index 994644793d5..ac52ba4bafa 100644 --- a/ts/features/messages/saga/index.ts +++ b/ts/features/messages/saga/index.ts @@ -1,35 +1,111 @@ import { SagaIterator } from "redux-saga"; import { - call, + fork, put, select, takeEvery, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; import { SessionToken } from "../../../types/SessionToken"; import { clearCache } from "../../../store/actions/profile"; import { logoutSuccess } from "../../../store/actions/authentication"; import { downloadAttachment, getMessageDataAction, - removeCachedAttachment + getMessagePrecondition, + loadMessageById, + loadMessageDetails, + loadNextPageMessages, + loadPreviousPageMessages, + loadThirdPartyMessage, + migrateToPaginatedMessages, + reloadAllMessages, + removeCachedAttachment, + upsertMessageStatusAttributes } from "../store/actions"; import { retryDataAfterFastLoginSessionExpirationSelector } from "../store/reducers/messageGetStatus"; +import { BackendClient } from "../../../api/backend"; import { handleDownloadAttachment } from "./handleDownloadAttachment"; import { handleClearAllAttachments, handleClearAttachment } from "./handleClearAttachments"; import { handleLoadMessageData } from "./handleLoadMessageData"; +import { handleLoadNextPageMessages } from "./handleLoadNextPageMessages"; +import { handleLoadPreviousPageMessages } from "./handleLoadPreviousPageMessages"; +import { handleReloadAllMessages } from "./handleReloadAllMessages"; +import { handleLoadMessageById } from "./handleLoadMessageById"; +import { handleLoadMessageDetails } from "./handleLoadMessageDetails"; +import { handleUpsertMessageStatusAttribues } from "./handleUpsertMessageStatusAttribues"; +import { handleMigrateToPagination } from "./handleMigrateToPagination"; +import { handleMessagePrecondition } from "./handleMessagePrecondition"; +import { handleThirdPartyMessage } from "./handleThirdPartyMessage"; /** - * Handle the message attachment requests + * Handle messages requests + * @param backendClient * @param bearerToken */ -export function* watchMessageAttachmentsSaga( +export function* watchMessagesSaga( + backendClient: BackendClient, bearerToken: SessionToken ): SagaIterator { + yield* takeLatest( + loadNextPageMessages.request, + handleLoadNextPageMessages, + backendClient.getMessages + ); + + yield* takeLatest( + loadPreviousPageMessages.request, + handleLoadPreviousPageMessages, + backendClient.getMessages + ); + + yield* takeLatest( + reloadAllMessages.request, + handleReloadAllMessages, + backendClient.getMessages + ); + + yield* takeEvery( + loadMessageById.request, + handleLoadMessageById, + backendClient.getMessage + ); + + yield* takeLatest( + loadMessageDetails.request, + handleLoadMessageDetails, + backendClient.getMessage + ); + + yield* takeLatest( + getMessagePrecondition.request, + handleMessagePrecondition, + backendClient.getThirdPartyMessagePrecondition + ); + + yield* takeLatest( + loadThirdPartyMessage.request, + handleThirdPartyMessage, + backendClient.getThirdPartyMessage + ); + + yield* takeEvery( + upsertMessageStatusAttributes.request, + handleUpsertMessageStatusAttribues, + backendClient.upsertMessageStatusAttributes + ); + + yield* fork(watchLoadMessageData); + + yield* takeLatest( + migrateToPaginatedMessages.request, + handleMigrateToPagination, + backendClient.upsertMessageStatusAttributes + ); + // handle the request for a new downloadAttachment yield* takeLatest( downloadAttachment.request, @@ -38,32 +114,17 @@ export function* watchMessageAttachmentsSaga( ); // handle the request for removing a downloaded attachment - yield* takeEvery( - removeCachedAttachment, - function* (action: ActionType) { - yield* call(handleClearAttachment, action); - } - ); + yield* takeEvery(removeCachedAttachment, handleClearAttachment); // handle the request for clearing user profile cache - yield* takeEvery(clearCache, function* () { - yield* call(handleClearAllAttachments); - }); + yield* takeEvery(clearCache, handleClearAllAttachments); // clear cache when user explicitly logs out - yield* takeEvery( - logoutSuccess, - function* (_: ActionType) { - yield* call(handleClearAllAttachments); - } - ); + yield* takeEvery(logoutSuccess, handleClearAllAttachments); } -export function* watchLoadMessageData() { - yield* takeLatest( - getType(getMessageDataAction.request), - handleLoadMessageData - ); +function* watchLoadMessageData() { + yield* takeLatest(getMessageDataAction.request, handleLoadMessageData); const retryDataOrUndefined = yield* select( retryDataAfterFastLoginSessionExpirationSelector diff --git a/ts/features/messages/saga/watchLoadMessageDetails.ts b/ts/features/messages/saga/watchLoadMessageDetails.ts deleted file mode 100644 index cbec0d38f66..00000000000 --- a/ts/features/messages/saga/watchLoadMessageDetails.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { put, takeLatest, call } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; - -import { CreatedMessageWithContentAndAttachments } from "../../../../definitions/backend/CreatedMessageWithContentAndAttachments"; -import { BackendClient } from "../../../api/backend"; -import { loadMessageDetails } from "../store/actions"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { getError } from "../../../utils/errors"; -import { toUIMessageDetails } from "../store/reducers/transformers"; -import { isTestEnv } from "../../../utils/environment"; -import { withRefreshApiCall } from "../../fastLogin/saga/utils"; -import { errorToReason, unknownToReason } from "../utils"; -import { trackLoadMessageDetailsFailure } from "../analytics"; -import { handleResponse } from "../utils/responseHandling"; - -type LocalActionType = ActionType<(typeof loadMessageDetails)["request"]>; -type LocalBeClient = ReturnType["getMessage"]; - -export default function* watcher( - getMessage: LocalBeClient -): Generator> { - yield* takeLatest( - getType(loadMessageDetails.request), - tryLoadMessageDetails(getMessage) - ); -} - -/** - * A saga to fetch a message from the Backend and save it in the redux store. - * - * @param getMessage - */ -function tryLoadMessageDetails(getMessage: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - const id = action.payload.id; - try { - const response = (yield* call( - withRefreshApiCall, - getMessage({ id }), - action - )) as unknown as SagaCallReturnType; - const nextAction = - handleResponse( - response, - (message: CreatedMessageWithContentAndAttachments) => - loadMessageDetails.success(toUIMessageDetails(message)), - error => { - const reason = errorToReason(error); - trackLoadMessageDetailsFailure(reason); - return loadMessageDetails.failure({ - id, - error: getError(error) - }); - } - ); - - if (nextAction) { - yield* put(nextAction); - } - } catch (error) { - const reason = unknownToReason(error); - trackLoadMessageDetailsFailure(reason); - yield* put( - loadMessageDetails.failure({ - id, - error: getError(error) - }) - ); - } - }; -} - -export const testTryLoadMessageDetails = isTestEnv - ? tryLoadMessageDetails - : undefined; diff --git a/ts/features/messages/saga/watchLoadNextPageMessages.ts b/ts/features/messages/saga/watchLoadNextPageMessages.ts deleted file mode 100644 index a82b3537e22..00000000000 --- a/ts/features/messages/saga/watchLoadNextPageMessages.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { put, takeLatest, call } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; -import { BackendClient } from "../../../api/backend"; -import { loadNextPageMessages as loadNextPageMessagesAction } from "../store/actions"; -import { toUIMessage } from "../store/reducers/transformers"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { isTestEnv } from "../../../utils/environment"; -import { convertUnknownToError, getError } from "../../../utils/errors"; -import { withRefreshApiCall } from "../../fastLogin/saga/utils"; -import { errorToReason, unknownToReason } from "../utils"; -import { trackLoadNextPageMessagesFailure } from "../analytics"; -import { handleResponse } from "../utils/responseHandling"; - -type LocalActionType = ActionType< - (typeof loadNextPageMessagesAction)["request"] ->; -type LocalBeClient = ReturnType["getMessages"]; - -export default function* watcher( - getMessages: LocalBeClient -): Generator> { - yield* takeLatest( - getType(loadNextPageMessagesAction.request), - tryLoadNextPageMessages(getMessages) - ); -} - -function tryLoadNextPageMessages(getMessages: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - const { filter, pageSize, cursor } = action.payload; - try { - const response = (yield* call( - withRefreshApiCall, - getMessages({ - enrich_result_data: true, - page_size: pageSize, - maximum_id: cursor, - archived: filter.getArchived - }), - action - )) as unknown as SagaCallReturnType; - const nextAction = handleResponse( - response, - ({ items, next }: PaginatedPublicMessagesCollection) => - loadNextPageMessagesAction.success({ - messages: items.map(toUIMessage), - pagination: { next }, - filter - }), - error => { - const reason = errorToReason(error); - trackLoadNextPageMessagesFailure(reason); - return loadNextPageMessagesAction.failure({ - error: getError(error), - filter - }); - } - ); - - if (nextAction) { - yield* put(nextAction); - } - } catch (e) { - const reason = unknownToReason(e); - trackLoadNextPageMessagesFailure(reason); - yield* put( - loadNextPageMessagesAction.failure({ - error: convertUnknownToError(e), - filter - }) - ); - } - }; -} - -export const testTryLoadNextPageMessages = isTestEnv - ? tryLoadNextPageMessages - : undefined; diff --git a/ts/features/messages/saga/watchLoadPreviousPageMessages.ts b/ts/features/messages/saga/watchLoadPreviousPageMessages.ts deleted file mode 100644 index a8cc123003e..00000000000 --- a/ts/features/messages/saga/watchLoadPreviousPageMessages.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { call, put, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { BackendClient } from "../../../api/backend"; -import { loadPreviousPageMessages as loadPreviousPageMessagesAction } from "../store/actions"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { toUIMessage } from "../store/reducers/transformers"; -import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; -import { isTestEnv } from "../../../utils/environment"; -import { convertUnknownToError, getError } from "../../../utils/errors"; -import { withRefreshApiCall } from "../../fastLogin/saga/utils"; -import { errorToReason, unknownToReason } from "../utils"; -import { trackLoadPreviousPageMessagesFailure } from "../analytics"; -import { handleResponse } from "../utils/responseHandling"; - -type LocalActionType = ActionType< - (typeof loadPreviousPageMessagesAction)["request"] ->; -type LocalBeClient = ReturnType["getMessages"]; - -export default function* watcher( - getMessages: LocalBeClient -): Generator> { - yield* takeLatest( - getType(loadPreviousPageMessagesAction.request), - tryLoadPreviousPageMessages(getMessages) - ); -} - -function tryLoadPreviousPageMessages(getMessages: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - const { filter, cursor, pageSize } = action.payload; - try { - const response = (yield* call( - withRefreshApiCall, - getMessages({ - enrich_result_data: true, - page_size: pageSize, - minimum_id: cursor, - archived: filter.getArchived - }), - action - )) as unknown as SagaCallReturnType; - const nextAction = handleResponse( - response, - ({ items, prev }: PaginatedPublicMessagesCollection) => - loadPreviousPageMessagesAction.success({ - messages: items.map(toUIMessage), - pagination: { previous: prev }, - filter - }), - error => { - const reason = errorToReason(error); - trackLoadPreviousPageMessagesFailure(reason); - return loadPreviousPageMessagesAction.failure({ - error: getError(error), - filter - }); - } - ); - - if (nextAction) { - yield* put(nextAction); - } - } catch (e) { - const reason = unknownToReason(e); - trackLoadPreviousPageMessagesFailure(reason); - yield* put( - loadPreviousPageMessagesAction.failure({ - error: convertUnknownToError(e), - filter - }) - ); - } - }; -} - -export const testTryLoadPreviousPageMessages = isTestEnv - ? tryLoadPreviousPageMessages - : undefined; diff --git a/ts/features/messages/saga/watchMigrateToPagination.ts b/ts/features/messages/saga/watchMigrateToPagination.ts deleted file mode 100644 index ca61d08c949..00000000000 --- a/ts/features/messages/saga/watchMigrateToPagination.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ValidationError } from "io-ts"; -import { call, put, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { MessageStatusArchivingChange } from "../../../../definitions/backend/MessageStatusArchivingChange"; -import { MessageStatusBulkChange } from "../../../../definitions/backend/MessageStatusBulkChange"; -import { BackendClient } from "../../../api/backend"; -import migrateToPagination from "../utils/migrateToPagination"; -import { migrateToPaginatedMessages, removeMessages } from "../store/actions"; -import { MessageStatus } from "../store/reducers/messagesStatus"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { isTestEnv } from "../../../utils/environment"; -import { readablePrivacyReport } from "../../../utils/reporters"; - -type LocalActionType = ActionType< - (typeof migrateToPaginatedMessages)["request"] ->; -type LocalBeClient = ReturnType< - typeof BackendClient ->["upsertMessageStatusAttributes"]; - -export default function* watcher( - putMessages: LocalBeClient -): Generator> { - yield* takeLatest( - getType(migrateToPaginatedMessages.request), - tryMigration(putMessages) - ); -} - -function tryMigration(putMessages: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - try { - const { bogus, toMigrate } = Object.keys(action.payload).reduce<{ - bogus: Array; - toMigrate: Array<{ id: string; isArchived: boolean; isRead: boolean }>; - }>( - (acc, id) => { - const status = action.payload[id]; - if (status && (status.isRead || status.isArchived)) { - return { - ...acc, - toMigrate: [ - ...acc.toMigrate, - { - id, - isRead: status.isRead, - isArchived: status.isArchived - } as { - id: string; - isArchived: boolean; - isRead: boolean; - } - ] - }; - } - return { ...acc, bogus: [...acc.bogus, id] }; - }, - { bogus: [], toMigrate: [] } - ); - - if (toMigrate.length === 0) { - yield* put(removeMessages(bogus)); - yield* put(migrateToPaginatedMessages.success(0)); - return; - } - - const { failed, succeeded } = yield* call( - migrateToPagination, - toMigrate, - (id: string, { isRead, isArchived }: MessageStatus) => { - if (isRead) { - return putMessages({ - id, - body: { - change_type: "bulk", - is_read: true, - is_archived: isArchived - } as MessageStatusBulkChange - }); - } - return putMessages({ - id, - body: { - change_type: "archiving", - is_archived: isArchived - } as MessageStatusArchivingChange - }); - } - ); - - yield* put(removeMessages(succeeded.concat(bogus))); - - if (failed.length === 0) { - yield* put(migrateToPaginatedMessages.success(succeeded.length)); - } else { - yield* put(migrateToPaginatedMessages.failure({ succeeded, failed })); - } - } catch (e) { - // assuming the worst, no messages were migrated because of an unexpected failure - const errorPayload = { - succeeded: [], - failed: Object.keys(action.payload).map(id => ({ - messageId: id, - - // FIXME: This is potentially unsafe. - error: readablePrivacyReport(e as Array) - })) - }; - yield* put(migrateToPaginatedMessages.failure(errorPayload)); - } - }; -} - -export const testTryLoadPreviousPageMessages = isTestEnv - ? tryMigration - : undefined; diff --git a/ts/features/messages/saga/watchReloadAllMessages.ts b/ts/features/messages/saga/watchReloadAllMessages.ts deleted file mode 100644 index 8738463c367..00000000000 --- a/ts/features/messages/saga/watchReloadAllMessages.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { call, put, takeLatest } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { BackendClient } from "../../../api/backend"; -import { reloadAllMessages as reloadAllMessagesAction } from "../store/actions"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { toUIMessage } from "../store/reducers/transformers"; -import { PaginatedPublicMessagesCollection } from "../../../../definitions/backend/PaginatedPublicMessagesCollection"; -import { isTestEnv } from "../../../utils/environment"; -import { getError } from "../../../utils/errors"; -import { withRefreshApiCall } from "../../fastLogin/saga/utils"; -import { errorToReason, unknownToReason } from "../utils"; -import { trackReloadAllMessagesFailure } from "../analytics"; -import { handleResponse } from "../utils/responseHandling"; - -type LocalActionType = ActionType<(typeof reloadAllMessagesAction)["request"]>; -type LocalBeClient = ReturnType["getMessages"]; - -export default function* watcher( - getMessages: LocalBeClient -): Generator> { - yield* takeLatest( - getType(reloadAllMessagesAction.request), - tryReloadAllMessages(getMessages) - ); -} - -function tryReloadAllMessages(getMessages: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - const { filter, pageSize } = action.payload; - try { - const response: SagaCallReturnType = (yield* call( - withRefreshApiCall, - getMessages({ - enrich_result_data: true, - page_size: pageSize, - archived: filter.getArchived - }), - action - )) as unknown as SagaCallReturnType; - const nextAction = handleResponse( - response, - ({ items, next, prev }: PaginatedPublicMessagesCollection) => - reloadAllMessagesAction.success({ - messages: items.map(toUIMessage), - pagination: { previous: prev, next }, - filter - }), - error => { - const reason = errorToReason(error); - trackReloadAllMessagesFailure(reason); - return reloadAllMessagesAction.failure({ - error: getError(error), - filter - }); - } - ); - - if (nextAction) { - yield* put(nextAction); - } - } catch (error) { - const reason = unknownToReason(error); - trackReloadAllMessagesFailure(reason); - yield* put( - reloadAllMessagesAction.failure({ - error: getError(error), - filter - }) - ); - } - }; -} - -export const testTryLoadPreviousPageMessages = isTestEnv - ? tryReloadAllMessages - : undefined; diff --git a/ts/features/messages/saga/watchUpsertMessageStatusAttribues.ts b/ts/features/messages/saga/watchUpsertMessageStatusAttribues.ts deleted file mode 100644 index 352b5801f3a..00000000000 --- a/ts/features/messages/saga/watchUpsertMessageStatusAttribues.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { put, takeEvery, call } from "typed-redux-saga/macro"; -import { ActionType, getType } from "typesafe-actions"; -import { MessageStatusArchivingChange } from "../../../../definitions/backend/MessageStatusArchivingChange"; -import { MessageStatusBulkChange } from "../../../../definitions/backend/MessageStatusBulkChange"; -import { MessageStatusChange } from "../../../../definitions/backend/MessageStatusChange"; -import { MessageStatusReadingChange } from "../../../../definitions/backend/MessageStatusReadingChange"; -import { BackendClient } from "../../../api/backend"; -import { - upsertMessageStatusAttributes, - UpsertMessageStatusAttributesPayload -} from "../store/actions"; -import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils"; -import { isTestEnv } from "../../../utils/environment"; -import { getError } from "../../../utils/errors"; -import { withRefreshApiCall } from "../../fastLogin/saga/utils"; -import { errorToReason, unknownToReason } from "../utils"; -import { trackUpsertMessageStatusAttributesFailure } from "../analytics"; -import { handleResponse } from "../utils/responseHandling"; - -type LocalActionType = ActionType< - (typeof upsertMessageStatusAttributes)["request"] ->; -type LocalBeClient = ReturnType< - typeof BackendClient ->["upsertMessageStatusAttributes"]; - -export default function* watcher( - putMessage: LocalBeClient -): Generator> { - yield* takeEvery( - getType(upsertMessageStatusAttributes.request), - tryUpsertMessageStatusAttributes(putMessage) - ); -} - -/** - * @throws invalid payload - * @param payload - */ -function validatePayload( - payload: UpsertMessageStatusAttributesPayload -): MessageStatusChange { - switch (payload.update.tag) { - case "archiving": - return { - change_type: "archiving", - is_archived: payload.update.isArchived - } as MessageStatusArchivingChange; - case "reading": - return { - change_type: "reading", - is_read: true - } as MessageStatusReadingChange; - case "bulk": - return { - change_type: "bulk", - is_read: true, - is_archived: payload.update.isArchived - } as MessageStatusBulkChange; - default: - throw new TypeError("invalid payload"); - } -} - -function tryUpsertMessageStatusAttributes(putMessage: LocalBeClient) { - return function* gen( - action: LocalActionType - ): Generator> { - try { - const body = validatePayload(action.payload); - const response = (yield* call( - withRefreshApiCall, - putMessage({ id: action.payload.message.id, body }), - action - )) as unknown as SagaCallReturnType; - - const nextAction = handleResponse( - response, - _ => upsertMessageStatusAttributes.success(action.payload), - error => { - const reason = errorToReason(error); - trackUpsertMessageStatusAttributesFailure(reason); - return upsertMessageStatusAttributes.failure({ - error: getError(error), - payload: action.payload - }); - } - ); - - if (nextAction) { - yield* put(nextAction); - } - } catch (error) { - const reason = unknownToReason(error); - trackUpsertMessageStatusAttributesFailure(reason); - yield* put( - upsertMessageStatusAttributes.failure({ - error: getError(error), - payload: action.payload - }) - ); - } - }; -} - -export const testTryUpsertMessageStatusAttributes = isTestEnv - ? tryUpsertMessageStatusAttributes - : undefined; diff --git a/ts/sagas/__tests__/initializeApplicationSaga.test.ts b/ts/sagas/__tests__/initializeApplicationSaga.test.ts index db72c70e28e..b2144a21a22 100644 --- a/ts/sagas/__tests__/initializeApplicationSaga.test.ts +++ b/ts/sagas/__tests__/initializeApplicationSaga.test.ts @@ -54,7 +54,9 @@ jest.mock("react-native-share", () => ({ open: jest.fn() })); -jest.mock("../../api/backend"); +jest.mock("../../api/backend", () => ({ + BackendClient: jest.fn().mockReturnValue({}) +})); const profile: InitializedProfile = { ...mockedProfile, @@ -106,15 +108,6 @@ describe("initializeApplicationSaga", () => { .next() .next() .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() .select(sessionInfoSelector) .next(O.none) .next(O.none) // loadSessionInformationSaga @@ -255,15 +248,6 @@ describe("initializeApplicationSaga", () => { .next() .next() .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() - .next() .select(sessionInfoSelector) .next( O.some({ diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 8b11a1b9949..1a2c0ac4b35 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -88,10 +88,7 @@ import { setProfileHashedFiscalCode } from "../store/actions/crossSessions"; import { handleClearAllAttachments } from "../features/messages/saga/handleClearAttachments"; -import { - watchLoadMessageData, - watchMessageAttachmentsSaga -} from "../features/messages/saga"; +import { watchMessagesSaga } from "../features/messages/saga"; import { watchPnSaga } from "../features/pn/store/sagas/watchPnSaga"; import { startupLoadSuccess } from "../store/actions/startup"; import { watchIDPaySaga } from "../features/idpay/common/saga"; @@ -110,15 +107,6 @@ import { } from "../store/reducers/backendStatus"; import { refreshSessionToken } from "../features/fastLogin/store/actions/tokenRefreshActions"; import { setSecurityAdviceReadyToShow } from "../features/fastLogin/store/actions/securityAdviceActions"; -import watchLoadMessageDetails from "../features/messages/saga/watchLoadMessageDetails"; -import watchLoadNextPageMessages from "../features/messages/saga/watchLoadNextPageMessages"; -import watchLoadPreviousPageMessages from "../features/messages/saga/watchLoadPreviousPageMessages"; -import watchMigrateToPagination from "../features/messages/saga/watchMigrateToPagination"; -import watchReloadAllMessages from "../features/messages/saga/watchReloadAllMessages"; -import watchUpsertMessageStatusAttribues from "../features/messages/saga/watchUpsertMessageStatusAttribues"; -import { watchLoadMessageById } from "../features/messages/saga/watchLoadMessageById"; -import { watchThirdPartyMessageSaga } from "../features/messages/saga/watchThirdPartyMessageSaga"; -import { watchMessagePrecondition } from "../features/messages/saga/watchMessagePrecondition"; import { startAndReturnIdentificationResult } from "./identification"; import { previousInstallationDataDeleteSaga } from "./installation"; import { @@ -330,25 +318,8 @@ export function* initializeApplicationSaga( // Load visible services and service details from backend when requested yield* fork(watchLoadServicesSaga, backendClient); - yield* fork(watchLoadNextPageMessages, backendClient.getMessages); - yield* fork(watchLoadPreviousPageMessages, backendClient.getMessages); - yield* fork(watchReloadAllMessages, backendClient.getMessages); - yield* fork(watchLoadMessageById, backendClient.getMessage); - yield* fork(watchLoadMessageDetails, backendClient.getMessage); - yield* fork( - watchMessagePrecondition, - backendClient.getThirdPartyMessagePrecondition - ); - yield* fork(watchThirdPartyMessageSaga, backendClient); - yield* fork( - watchUpsertMessageStatusAttribues, - backendClient.upsertMessageStatusAttributes - ); - yield* fork(watchLoadMessageData); - yield* fork( - watchMigrateToPagination, - backendClient.upsertMessageStatusAttributes - ); + // Start watching for Messages actions + yield* fork(watchMessagesSaga, backendClient, sessionToken); // watch FCI saga yield* fork(watchFciSaga, sessionToken, keyInfo); @@ -595,10 +566,6 @@ export function* initializeApplicationSaga( yield* fork(watchPnSaga, sessionToken, backendClient.getVerificaRpt); } - // Start watching for message attachments actions (general - // third-party message attachments and PN attachments) - yield* fork(watchMessageAttachmentsSaga, sessionToken); - const idPayTestEnabled: ReturnType = yield* select(isIdPayTestEnabledSelector);