diff --git a/docs/APIDOCUMENTATION.md b/docs/APIDOCUMENTATION.md index a814eded00..ba46953af9 100644 --- a/docs/APIDOCUMENTATION.md +++ b/docs/APIDOCUMENTATION.md @@ -97,40 +97,47 @@ --- -#### loadMembers() +#### loadMembers(clientLocale)
Returns an array of members associated with a specific user +##### Parameters + +> | name | type | data type | description | +> | -------------- | -------- | --------- | ------------------------- | +> | `clientLocale` | optional | string | The locale for the widget | + ##### Responses -> | http code | content-type | response | -> | ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -> | `200` | `application/json` | `{"members": [{ "aggregated_at": "2016-10-13T18:07:57.000Z","background_aggregation_is_disabled": false"connection_status":"CONNECTED","guid": "MBR-123","id": "unique_id","institution_code": "testbank","is_being_aggregated": false,"is_managed_by_user": false,"is_manual": false,"is_oauth": false,"metadata": "\\\"credentials_last_refreshed_at\\\": \\\"2015-10-15\\\"","most_recent_job_detail_code": null,"most_recent_job_detail_text": "","name": "Test Bank","oauth_window_uri": "https://testbank.com/oauth/authorize?client_id=b8OikQ4Ep3NuSUrQ13DdvFuwpNx-qqoAsJDVAQCy&redirect_uri=https%3A%2F%2Ftest.com%2Foauth%2Fredirect_from&response_type=code&scope=openid&state=d745bd4ee6f0f9c184757f574bcc2df2""successfully_aggregated_at": "2016-10-13T17:57:38.000Z","user_guid": "USR-123","user_id": "user123"}]}` | -> | `40#` | `application/json` | `{"response": {"status": 40#, "data": {}}}` | -> | `VerifyNotEnabled` | `application/json` | `instanceof Error + {"entity_type": "member", "name": "VerifyNotEnabled", "message": "This connection doesn't support verification."}` | +> | http code | content-type | response | +> | ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +> | `200` | `application/json` | `{"members": [{ "aggregated_at": "2016-10-13T18:07:57.000Z","background_aggregation_is_disabled": false"connection_status":"CONNECTED", "error": { "error_code": 1000, "error_message": "Test error message.", "error_type": "MEMBER", "locale": "en", "user_message": "Test user message." }, "guid": "MBR-123","id": "unique_id","institution_code": "testbank","is_being_aggregated": false,"is_managed_by_user": false,"is_manual": false,"is_oauth": false,"metadata": "\\\"credentials_last_refreshed_at\\\": \\\"2015-10-15\\\"","most_recent_job_detail_code": null,"most_recent_job_detail_text": "","name": "Test Bank","oauth_window_uri": "https://testbank.com/oauth/authorize?client_id=b8OikQ4Ep3NuSUrQ13DdvFuwpNx-qqoAsJDVAQCy&redirect_uri=https%3A%2F%2Ftest.com%2Foauth%2Fredirect_from&response_type=code&scope=openid&state=d745bd4ee6f0f9c184757f574bcc2df2""successfully_aggregated_at": "2016-10-13T17:57:38.000Z","user_guid": "USR-123","user_id": "user123"}]}` | +> | `40#` | `application/json` | `{"response": {"status": 40#, "data": {}}}` | +> | `VerifyNotEnabled` | `application/json` | `instanceof Error + {"entity_type": "member", "name": "VerifyNotEnabled", "message": "This connection doesn't support verification."}` |
--- -#### loadMemberByGuid(memberGuid) +#### loadMemberByGuid(memberGuid, clientLocale)
Returns a specific member by its guid ##### Parameters -> | name | type | data type | description | -> | ------------ | -------- | --------- | ------------------------ | -> | `memberGuid` | required | string | The specific member guid | +> | name | type | data type | description | +> | -------------- | -------- | --------- | ------------------------- | +> | `memberGuid` | required | string | The specific member guid | +> | `clientLocale` | optional | string | The locale for the widget | ##### Responses -> | http code | content-type | response | -> | --------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -> | `200` | `application/json` | `{"member": { "aggregated_at": "2016-10-13T18:07:57.000Z","background_aggregation_is_disabled": false"connection_status":"CONNECTED","guid": "MBR-123","id": "unique_id","institution_code": "testbank","is_being_aggregated": false,"is_managed_by_user": false,"is_manual": false,"is_oauth": false,"metadata": "\\\"credentials_last_refreshed_at\\\": \\\"2015-10-15\\\"","most_recent_job_detail_code": null,"most_recent_job_detail_text": "","name": "Test Bank","oauth_window_uri": "https://testbank.com/oauth/authorize?client_id=b8OikQ4Ep3NuSUrQ13DdvFuwpNx-qqoAsJDVAQCy&redirect_uri=https%3A%2F%2Ftest.com%2Foauth%2Fredirect_from&response_type=code&scope=openid&state=d745bd4ee6f0f9c184757f574bcc2df2""successfully_aggregated_at": "2016-10-13T17:57:38.000Z","user_guid": "USR-123","user_id": "user123"}}` | -> | `40#` | `application/json` | `{"response": {"status": 40#, "data": {"message": "Test message here"}}}` | +> | http code | content-type | response | +> | --------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +> | `200` | `application/json` | `{"member": { "aggregated_at": "2016-10-13T18:07:57.000Z","background_aggregation_is_disabled": false"connection_status":"CONNECTED", "error": { "error_code": 1000, "error_message": "Test error message.", "error_type": "MEMBER", "locale": "en", "user_message": "Test user message." }, "guid": "MBR-123","id": "unique_id","institution_code": "testbank","is_being_aggregated": false,"is_managed_by_user": false,"is_manual": false,"is_oauth": false,"metadata": "\\\"credentials_last_refreshed_at\\\": \\\"2015-10-15\\\"","most_recent_job_detail_code": null,"most_recent_job_detail_text": "","name": "Test Bank","oauth_window_uri": "https://testbank.com/oauth/authorize?client_id=b8OikQ4Ep3NuSUrQ13DdvFuwpNx-qqoAsJDVAQCy&redirect_uri=https%3A%2F%2Ftest.com%2Foauth%2Fredirect_from&response_type=code&scope=openid&state=d745bd4ee6f0f9c184757f574bcc2df2""successfully_aggregated_at": "2016-10-13T17:57:38.000Z","user_guid": "USR-123","user_id": "user123"}}` | +> | `40#` | `application/json` | `{"response": {"status": 40#, "data": {"message": "Test message here"}}}` |
diff --git a/src/components/RenderConnectStep.js b/src/components/RenderConnectStep.js index 709c99d5cf..9a5ba14e34 100644 --- a/src/components/RenderConnectStep.js +++ b/src/components/RenderConnectStep.js @@ -292,8 +292,8 @@ const RenderConnectStep = (props) => { ) } else if (step === STEPS.ACTIONABLE_ERROR) { // We are slowly adding codes and statuses to use ActionableError instead of LoginError. - const jobDetailCode = currentMember?.most_recent_job_detail_code ?? null - connectStepView = canHandleActionableError(jobDetailCode) ? ( + const errorCode = currentMember?.error?.error_code ?? null + connectStepView = canHandleActionableError(errorCode) ? ( ) : ( Promise<{ member: MemberResponseType }> deleteMember: (member: MemberDeleteType) => Promise getMemberCredentials: (memberGuid: string) => Promise - loadMemberByGuid?: (guid: string) => Promise - loadMembers: () => Promise + loadMemberByGuid?: (guid: string, clientLocale?: string) => Promise + loadMembers: (clientLocale?: string) => Promise updateMember: ( member: object, config: ClientConfigType, diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index 0763ac07f1..d2e17724b7 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useEffect, useState, useCallback } from 'react' +import { useEffect, useState, useCallback, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' import { from, of, defer } from 'rxjs' import { catchError, mergeMap, map } from 'rxjs/operators' @@ -47,6 +47,9 @@ export const getErrorResource = (err: { config: { url: string | string[] } }) => const useLoadConnect = () => { const { api } = useApi() const profiles = useSelector((state: RootState) => state.profiles) + const clientLocale = useMemo(() => { + return document.querySelector('html')?.getAttribute('lang') || 'en' + }, [document.querySelector('html')?.getAttribute('lang')]) const [config, setConfig] = useState({} as ClientConfigType) const dispatch = useDispatch() @@ -58,7 +61,7 @@ const useLoadConnect = () => { let request$ if (config.current_member_guid) { - request$ = loadConnectFromMemberConfig(config, api) + request$ = loadConnectFromMemberConfig(config, api, clientLocale) } else if (config.current_institution_guid || config.current_institution_code) { request$ = loadConnectFromInstitutionConfig(config, api) } else if (config.mode === VERIFY_MODE && config.current_microdeposit_guid) { @@ -71,8 +74,8 @@ const useLoadConnect = () => { .pipe( mergeMap((dependencies) => { if (clientSupportRequestedProducts(config, profiles.clientProfile)) { - return from(api.loadMembers()).pipe( - map((members) => + return from(api.loadMembers(clientLocale)).pipe( + map((members = []) => loadConnectSuccess({ members, widgetProfile: profiles.widgetProfile, @@ -139,8 +142,12 @@ export default useLoadConnect * Load the data for the configured member. Dispatch an error if * member's institution does not support the requested products */ -function loadConnectFromMemberConfig(config: ClientConfigType, api: ApiContextTypes) { - return from(api.loadMemberByGuid!(config.current_member_guid as string)).pipe( +function loadConnectFromMemberConfig( + config: ClientConfigType, + api: ApiContextTypes, + clientLocale: string, +) { + return from(api.loadMemberByGuid!(config.current_member_guid as string, clientLocale)).pipe( mergeMap((member: any) => { return defer(() => api.loadInstitutionByGuid(member.institution_guid)).pipe( map((institution) => { diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index e89ae311da..456ebd90eb 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -45,7 +45,7 @@ const loadConnect = (state, { payload }) => { const loadConnectSuccess = (state, action) => { const { - members, + members = [], member, microdeposit, config = {}, @@ -518,8 +518,7 @@ function getStepFromMember(member) { const connection_status = member.connection_status if ( - (member?.most_recent_job_detail_code && - canHandleActionableError(member?.most_recent_job_detail_code)) || + (member?.error?.error_code && canHandleActionableError(member?.error?.error_code)) || hasNoSingleAccountSelectOptions(member) ) // They configured connect with a member in error or missing SAS options. diff --git a/src/redux/reducers/__tests__/Connect-test.js b/src/redux/reducers/__tests__/Connect-test.js index 6cf64e744f..d3709b5b33 100644 --- a/src/redux/reducers/__tests__/Connect-test.js +++ b/src/redux/reducers/__tests__/Connect-test.js @@ -408,9 +408,15 @@ describe('Connect redux store', () => { const config = { mode: VERIFY_MODE, current_member_guid: 'MBR-1' } const member = { connection_status: ReadableStatuses.CONNECTED, + error: { + error_code: 1000, + error_message: 'Test', + error_type: 'MEMBER', + locale: 'en', + user_message: 'Test', + }, is_oauth: false, guid: 'MBR-1', - most_recent_job_detail_code: 1000, } const members = [member] const afterState = reducer( diff --git a/src/services/mockedData.ts b/src/services/mockedData.ts index 7a65a46f39..cad138f685 100644 --- a/src/services/mockedData.ts +++ b/src/services/mockedData.ts @@ -126,6 +126,7 @@ export const member = { member: { aggregation_status: 0, connection_status: 0, + error: null, guid: 'MBR-123', institution_guid: 'INS-123', institution_name: null, diff --git a/src/utilities/pollers.js b/src/utilities/pollers.js index 0c4ec344a4..7402dc0423 100644 --- a/src/utilities/pollers.js +++ b/src/utilities/pollers.js @@ -26,12 +26,12 @@ export const DEFAULT_POLLING_STATE = { userMessage: CONNECTING_MESSAGES.STARTING, // message to show the end user } -export function pollMember(memberGuid, api, onPostMessage, sendPosthogEvent) { +export function pollMember(memberGuid, api, onPostMessage, sendPosthogEvent, clientLocale) { return interval(3000).pipe( switchMap(() => // Poll the currentMember. Catch errors but don't handle it here // the scan will handle it below - defer(() => api.loadMemberByGuid(memberGuid)).pipe( + defer(() => api.loadMemberByGuid(memberGuid, clientLocale)).pipe( mergeMap((member) => defer(() => api.loadJob(member.most_recent_job_guid)).pipe( map((job) => { diff --git a/src/views/actionableError/ActionableError.tsx b/src/views/actionableError/ActionableError.tsx index dbdac4fedf..2cfc752380 100644 --- a/src/views/actionableError/ActionableError.tsx +++ b/src/views/actionableError/ActionableError.tsx @@ -19,7 +19,7 @@ export const ActionableError = () => { const institution = useSelector((state: RootState) => state.connect.selectedInstitution) const currentMember = useSelector(getCurrentMember) const initialConfig = useSelector(selectInitialConfig) - const jobDetailCode = currentMember.most_recent_job_detail_code + const jobDetailCode = currentMember.error.error_code const tokens = useTokens() const styles = getStyles(tokens) const getNextDelay = getDelay() @@ -30,11 +30,6 @@ export const ActionableError = () => { () => ({ [ACTIONABLE_ERROR_CODES.NO_ELIGIBLE_ACCOUNTS]: { title: __('No eligible accounts'), - userMessage: (institution: InstitutionResponseType) => - __( - 'Only checking or savings accounts can be used for transfers. If you have one at %1, make sure to select it when connecting. Otherwise, try connecting a different institution.', - institution.name, - ), primaryAction: { label: __('Log in again'), action: () => dispatch({ type: ActionTypes.ACTIONABLE_ERROR_LOG_IN_AGAIN }), @@ -95,7 +90,7 @@ export const ActionableError = () => { truncate={false} variant="Paragraph" > - {messagingMap[jobDetailCode].userMessage(institution)} + {currentMember.error.user_message} diff --git a/src/views/actionableError/__tests__/ActionableError-test.tsx b/src/views/actionableError/__tests__/ActionableError-test.tsx index 1da8cc5582..1a47d3ec82 100644 --- a/src/views/actionableError/__tests__/ActionableError-test.tsx +++ b/src/views/actionableError/__tests__/ActionableError-test.tsx @@ -4,7 +4,6 @@ import { render, screen } from 'src/utilities/testingLibrary' import { ActionableError } from 'src/views/actionableError/ActionableError' import { initialState as defaultState } from 'src/services/mockedData' import { STEPS } from 'src/const/Connect' -import { ACTIONABLE_ERROR_CODES } from '../consts' const institutionMock = { name: 'Institution', @@ -13,13 +12,20 @@ const institutionMock = { const membersMock = [ { guid: 'MEM-123', + error: { + error_code: 1000, + error_message: 'This Member has no eligible checking, savings, or money market accounts.', + error_type: 'MEMBER', + locale: 'en', + user_message: + "We couldn't find any accounts eligible for transfers. Please link a checking or savings account.", + }, name: 'Member', - most_recent_job_detail_code: ACTIONABLE_ERROR_CODES.NO_ELIGIBLE_ACCOUNTS, }, { guid: 'MEM-456', + error: null, name: 'Member', - most_recent_job_detail_code: null, }, ] @@ -54,7 +60,7 @@ describe('ActionableError', () => { expect(screen.getByText('No eligible accounts')).toBeInTheDocument() expect( screen.getByText( - `Only checking or savings accounts can be used for transfers. If you have one at ${institutionMock.name}, make sure to select it when connecting. Otherwise, try connecting a different institution.`, + "We couldn't find any accounts eligible for transfers. Please link a checking or savings account.", ), ).toBeInTheDocument() }) diff --git a/src/views/connecting/Connecting.js b/src/views/connecting/Connecting.js index 366892e4c0..8bfc192a2c 100644 --- a/src/views/connecting/Connecting.js +++ b/src/views/connecting/Connecting.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, useContext } from 'react' +import React, { useEffect, useState, useRef, useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { defer, of } from 'rxjs' import { @@ -60,7 +60,9 @@ export const Connecting = (props) => { const selectedInstitution = useSelector(getSelectedInstitution) const sendPosthogEvent = useAnalyticsEvent() - + const clientLocale = useMemo(() => { + return document.querySelector('html')?.getAttribute('lang') || 'en' + }, [document.querySelector('html')?.getAttribute('lang')]) const currentMember = useSelector(getCurrentMember) const isComboJobsEnabled = useSelector(isConnectComboJobsEnabled) const jobSchedule = useSelector((state) => state.connect.jobSchedule) @@ -245,7 +247,7 @@ export const Connecting = (props) => { const startJob$ = defer(() => api.runJob(activeJob?.type, currentMember.guid, connectConfig, true), ).pipe( - mergeMap(() => api.loadMemberByGuid(currentMember.guid)), + mergeMap(() => api.loadMemberByGuid(currentMember.guid, clientLocale)), catchError((error) => { // We control the scenarios of a 409 error (job already running, or member already exists). @@ -268,7 +270,7 @@ export const Connecting = (props) => { }) .pipe( concatMap((member) => - pollMember(member.guid, api, onPostMessage, sendPosthogEvent).pipe( + pollMember(member.guid, api, onPostMessage, sendPosthogEvent, clientLocale).pipe( tap((pollingState) => handleMemberPoll(pollingState)), filter((pollingState) => pollingState.jobIsDone), pluck('currentResponse'), diff --git a/src/views/credentials/CreateMemberForm.js b/src/views/credentials/CreateMemberForm.js index 9f4171dd35..833b7642b7 100644 --- a/src/views/credentials/CreateMemberForm.js +++ b/src/views/credentials/CreateMemberForm.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useContext } from 'react' +import React, { useEffect, useState, useContext, useMemo } from 'react' import PropTypes from 'prop-types' import { defer, of } from 'rxjs' import { catchError, delay, map, mergeMap } from 'rxjs/operators' @@ -32,6 +32,9 @@ export const CreateMemberForm = (props) => { const config = useSelector(selectConfig) const isHuman = useSelector((state) => state.app.humanEvent) const currentMembers = useSelector((state) => state.connect.members) + const clientLocale = useMemo(() => { + return document.querySelector('html')?.getAttribute('lang') || 'en' + }, [document.querySelector('html')?.getAttribute('lang')]) const [isCreatingMember, setIsCreatingMember] = useState(false) const [memberCreateError, setMemberCreateError] = useState(null) @@ -127,7 +130,7 @@ export const CreateMemberForm = (props) => { } }), ) - return defer(() => api.loadMemberByGuid(memberGuid)).pipe( + return defer(() => api.loadMemberByGuid(memberGuid, clientLocale)).pipe( mergeMap((member) => { const shouldStepToMFA = member.connection_status === ReadableStatuses.CHALLENGED return shouldStepToMFA ? stepToMFA$(member) : updateMember$ diff --git a/src/views/manualAccount/ManualAccountForm.tsx b/src/views/manualAccount/ManualAccountForm.tsx index c6f30b789d..c7fb975890 100644 --- a/src/views/manualAccount/ManualAccountForm.tsx +++ b/src/views/manualAccount/ManualAccountForm.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React, { MutableRefObject, useEffect, useState, useRef } from 'react' +import React, { MutableRefObject, useEffect, useState, useRef, useMemo } from 'react' import { useSelector, useDispatch } from 'react-redux' import { from, of, zip, defer } from 'rxjs' @@ -51,6 +51,9 @@ export const ManualAccountForm = React.forwardRef { + return document.querySelector('html')?.getAttribute('lang') || 'en' + }, [document.querySelector('html')?.getAttribute('lang')]) const [saving, setSaving] = useState(false) const [isPersonal, setIsPersonal] = useState(true) const [returnField, setReturnField] = useState(null) @@ -168,7 +171,7 @@ export const ManualAccountForm = React.forwardRef { diff --git a/typings/apiTypes.d.ts b/typings/apiTypes.d.ts index 8109a4fd05..5dd8f027d4 100644 --- a/typings/apiTypes.d.ts +++ b/typings/apiTypes.d.ts @@ -81,6 +81,13 @@ type MemberDeleteType = { type MemberResponseType = { aggregation_status: number connection_status: number + error?: { + error_code: number + error_message: string + error_type: string + locale: string + user_message: string + } guid: string institution_guid: string institution_name?: string | null diff --git a/typings/mxTypes.d.ts b/typings/mxTypes.d.ts index 535cf86ba6..589e2d8b55 100644 --- a/typings/mxTypes.d.ts +++ b/typings/mxTypes.d.ts @@ -22,6 +22,13 @@ type MemberDeleteType = { type MemberResponseType = { aggregation_status: number connection_status: number + error?: { + error_code: number + error_message: string + error_type: string + locale: string + user_message: string + } guid: string institution_guid: string institution_name?: string | null