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