From 030f25335b92fb08438c7bfc7743bcbfb88f3773 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 30 Apr 2026 13:59:24 -0600 Subject: [PATCH 1/5] feat(oauth): restore update flow and handle new inbound members --- docs/APIDOCUMENTATION.md | 6 +- docs/USER_FEATURES.md | 10 + src/ConnectWidget.tsx | 9 +- src/__tests__/ConnectWidget-test.tsx | 99 +++++++- src/const/apiProviderMock.ts | 1 + src/redux/actions/Connect.js | 5 + src/utilities/testingLibrary.tsx | 1 + src/views/oauth/OAuthStep.js | 14 +- src/views/oauth/WaitingForOAuth.js | 35 ++- src/views/oauth/__tests__/OAuthStep-test.tsx | 75 +++++- .../oauth/__tests__/WaitingForOAuth-test.tsx | 26 ++ tmp/planning/CTT-178/CTT-178-refined.md | 36 +++ tmp/planning/CTT-178/CTT-178.md | 44 ++++ tmp/planning/CTT-178/baseline.txt | 238 ++++++++++++++++++ tmp/planning/CTT-178/plan.md | 180 +++++++++++++ tmp/planning/CTT-178/tasks.md | 53 ++++ 16 files changed, 807 insertions(+), 25 deletions(-) create mode 100644 tmp/planning/CTT-178/CTT-178-refined.md create mode 100644 tmp/planning/CTT-178/CTT-178.md create mode 100644 tmp/planning/CTT-178/baseline.txt create mode 100644 tmp/planning/CTT-178/plan.md create mode 100644 tmp/planning/CTT-178/tasks.md diff --git a/docs/APIDOCUMENTATION.md b/docs/APIDOCUMENTATION.md index 50a10cf11f..ed330d3445 100644 --- a/docs/APIDOCUMENTATION.md +++ b/docs/APIDOCUMENTATION.md @@ -132,6 +132,10 @@ > | `memberGuid` | required | string | The specific member guid | > | `clientLocale` | optional | string | The locale for the widget | +##### Notes + +> This callback is also used during OAuth flows to synchronize member data when the backend returns a different `inbound_member_guid` than the one used to start the flow (e.g., during non-OAuth to OAuth migrations). When this happens, the widget will fetch the new member record and update its internal state to use the new GUID. + ##### Responses > | http code | content-type | response | @@ -619,7 +623,7 @@ xee > | name | type | data type | description | > | ------------ | -------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | -> | `memberGuid` | optional | string | | +> | `memberGuid` | optional | string | The GUID of the member to update. If provided, the widget will initiate an OAuth update flow for this member. | > | `config` | required | [`ClientConfigType`](../typings/connectProps.d.ts#L19) | The connect widget uses the config to set the initial state and behavior of the widget. [More details](./CLIENT_CONFIG.md) | ##### Responses diff --git a/docs/USER_FEATURES.md b/docs/USER_FEATURES.md index 649018a58a..f9977c878b 100644 --- a/docs/USER_FEATURES.md +++ b/docs/USER_FEATURES.md @@ -27,6 +27,16 @@ const userFeatures = [ | `CONNECT_COMBO_JOBS` | When enabled, the Connect widget will create COMBINATION jobs instead of individual jobs (aggregate, verification, reward, etc). |
{
 feature_name: 'CONNECT_COMBO_JOBS',
 guid: 'FTR-123',
 is_enabled: true
 }
| + +## OAuth Member Synchronization + +When updating a member via OAuth, it is possible for the backend to return a different member GUID (`inbound_member_guid`) than the one used to initiate the flow. This commonly occurs during migrations from non-OAuth to OAuth connections, or when a user signs in with a different set of credentials at the same institution. + +The Connect Widget handles this synchronization automatically by: +1. Detecting the GUID change upon successful completion of the OAuth flow. +2. Fetching the new member's full record using the `loadMemberByGuid` callback. +3. Updating the internal Redux state to reflect the new `currentMemberGuid` and including the new member record in the list of active members. +4. Seamlessly transitioning the user to the `Connecting` step with the synchronized member data.
[<-- Back to README](../README.md#props) diff --git a/src/ConnectWidget.tsx b/src/ConnectWidget.tsx index 97acf4bf03..a9ea09faaa 100644 --- a/src/ConnectWidget.tsx +++ b/src/ConnectWidget.tsx @@ -19,26 +19,23 @@ interface PostMessageContextType { export const PostMessageContext = createContext({ onPostMessage: () => {} }) -function setupLocalizedContent(localizedContent: Record) { - Store.dispatch(setLocalizedContent(localizedContent)) -} - export const ConnectWidget = ({ onPostMessage = () => {}, onAnalyticPageview = () => {}, postMessageEventOverrides, showTooSmallDialog = true, webSocketConnection, + store = Store, ...props }: any) => { initGettextLocaleData(props.language) useEffect(() => { - setupLocalizedContent(props?.language?.localizedContent || {}) + store.dispatch(setLocalizedContent(props?.language?.localizedContent || {})) }, []) return ( - + diff --git a/src/__tests__/ConnectWidget-test.tsx b/src/__tests__/ConnectWidget-test.tsx index 6bb41a5ad5..6033e76a7c 100644 --- a/src/__tests__/ConnectWidget-test.tsx +++ b/src/__tests__/ConnectWidget-test.tsx @@ -1,13 +1,15 @@ import React from 'react' -import { describe, it, expect, vi } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' import { Subject } from 'rxjs' import { act } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { ConnectWidget } from '../ConnectWidget' -import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { render, screen, waitFor, createTestReduxStore } from 'src/utilities/testingLibrary' import { apiValue as apiValueMock } from 'src/const/apiProviderMock' -import { member, JOB_DATA } from 'src/services/mockedData' +import { member, JOB_DATA, OAUTH_STATE, initialState, masterData } from 'src/services/mockedData' import { ReadableStatuses } from 'src/const/Statuses' +import { STEPS } from 'src/const/Connect' // Mock react-confetti to avoid Canvas issues in JSDOM vi.mock('react-confetti', () => ({ @@ -22,6 +24,13 @@ describe('ConnectWidget', () => { language: { locale: 'en', localizedContent: {} }, } + let activeStore = createTestReduxStore() + + beforeEach(() => { + // Reset the active store before each test + activeStore = createTestReduxStore() + }) + it('renders the real Connect widget and handles WebSocket messages correctly', async () => { const webSocketMessages$ = new Subject() const mockWS = { @@ -50,9 +59,10 @@ describe('ConnectWidget', () => { clientConfig={clientConfig} experimentalFeatures={{ useWebSockets: true }} onSuccessfulAggregation={onSuccessfulAggregation} + store={activeStore} webSocketConnection={mockWS} />, - { apiValue: mockApiValue }, + { apiValue: mockApiValue, store: activeStore }, ) // The widget should enter the Connecting state @@ -79,4 +89,85 @@ describe('ConnectWidget', () => { ) }) }) + + it('handles OAuth migration flow where member GUID changes', async () => { + const oldMember = { + ...member.member, + guid: 'MBR-OLD', + is_oauth: true, + connection_status: ReadableStatuses.DENIED, + } + const newMember = { ...member.member, guid: 'MBR-NEW', is_oauth: true, name: 'New Member' } + + const loadOAuthStates = vi + .fn() + .mockResolvedValue([{ ...OAUTH_STATE.oauth_state, guid: 'OAS-123', auth_status: 1 }]) + const loadOAuthState = vi.fn().mockResolvedValue({ + ...OAUTH_STATE.oauth_state, + guid: 'OAS-123', + auth_status: 2, + inbound_member_guid: 'MBR-NEW', + }) + const loadMemberByGuid = vi.fn().mockImplementation((guid) => { + if (guid === 'MBR-OLD') return Promise.resolve(oldMember) + if (guid === 'MBR-NEW') return Promise.resolve(newMember) + return Promise.resolve({}) + }) + + const mockApiValue = { + ...apiValueMock, + loadOAuthStates, + loadOAuthState, + loadMemberByGuid, + } + + const clientConfig = { mode: 'aggregation', current_member_guid: 'MBR-OLD' } + + const preloadedState = { + profiles: initialState.profiles, + connect: { + ...initialState.connect, + members: [oldMember], + currentMemberGuid: 'MBR-OLD', + location: [{ step: STEPS.ENTER_CREDENTIALS }], // Start at OAuth Step + }, + } + + // Create a new store with preloaded state + activeStore = createTestReduxStore(preloadedState) + + render( + , + { + apiValue: mockApiValue, + store: activeStore, + }, + ) + + // Verify we are on OAuth Step + expect(await screen.findByText(/Log in at/i)).toBeInTheDocument() + + // Click continue to start waiting for OAuth + const loginButton = await screen.findByTestId('continue-button') + await userEvent.click(loginButton) + + // Now it should be in WaitingForOAuth, then finish polling, then fetch new member, then go to Connecting + expect(await screen.findByText(/Waiting for permission/i)).toBeInTheDocument() + + // Verify it transitions to Connecting with the NEW GUID + await waitFor( + () => { + expect(screen.getByText(/Connecting to/i)).toBeInTheDocument() + const state = activeStore.getState() + expect(state.connect.currentMemberGuid).toBe('MBR-NEW') + expect(state.connect.members).toContainEqual(newMember) + }, + { timeout: 15000 }, + ) + }, 35000) }) diff --git a/src/const/apiProviderMock.ts b/src/const/apiProviderMock.ts index 8b6d1e12e0..fae532e437 100644 --- a/src/const/apiProviderMock.ts +++ b/src/const/apiProviderMock.ts @@ -22,6 +22,7 @@ export const apiValue: ApiContextTypes = { loadInstitutionByGuid: () => Promise.resolve(institutionData.institution), oAuthStart: () => Promise.resolve(), updateMFA: () => Promise.resolve(member.member), + loadMemberByGuid: () => Promise.resolve(member.member), loadJob: () => Promise.resolve(JOB_DATA), runJob: () => Promise.resolve(member.member), loadOAuthStates: () => Promise.resolve([OAUTH_STATE.oauth_state]), diff --git a/src/redux/actions/Connect.js b/src/redux/actions/Connect.js index e772d1aae8..f8a436b222 100644 --- a/src/redux/actions/Connect.js +++ b/src/redux/actions/Connect.js @@ -102,6 +102,11 @@ export const handleOAuthSuccess = (memberGuid) => ({ payload: memberGuid, }) +export const updateMemberSuccess = (member) => ({ + type: ActionTypes.UPDATE_MEMBER_SUCCESS, + payload: { item: member }, +}) + export const deleteMemberSuccess = (memberGuid) => ({ type: ActionTypes.DELETE_MEMBER_SUCCESS, payload: { memberGuid }, diff --git a/src/utilities/testingLibrary.tsx b/src/utilities/testingLibrary.tsx index 3927686f7a..382ba554e7 100644 --- a/src/utilities/testingLibrary.tsx +++ b/src/utilities/testingLibrary.tsx @@ -86,6 +86,7 @@ const renderWithUser = ( ...options, }), user: userEvent.setup(), + store, } } diff --git a/src/views/oauth/OAuthStep.js b/src/views/oauth/OAuthStep.js index 24ef42d581..665cf7d95d 100644 --- a/src/views/oauth/OAuthStep.js +++ b/src/views/oauth/OAuthStep.js @@ -138,7 +138,10 @@ export const OAuthStep = React.forwardRef((props, navigationRef) => { * if (member && member.is_oauth && api.getOAuthWindowURI) { * member$ = of(member) */ - if (pendingOauthMember) { + if (member?.guid) { + // If there is an existing member, don't create a new one, use that one (restores update flow) + member$ = of(member) + } else if (pendingOauthMember) { // If there is a pending oauth member, don't create a new one, use that one member$ = of(pendingOauthMember) } else { @@ -216,9 +219,14 @@ export const OAuthStep = React.forwardRef((props, navigationRef) => { setIsWaitingForOAuth(false) } - function handleOAuthSuccess(memberGuid) { + function handleOAuthSuccess(memberGuid, member = null) { closeOAuthWindow() - dispatch(connectActions.handleOAuthSuccess(memberGuid)) + + if (member) { + dispatch(connectActions.updateMemberSuccess(member)) + } else { + dispatch(connectActions.handleOAuthSuccess(memberGuid)) + } } function handleOAuthError(memberGuid, errorReason = null) { diff --git a/src/views/oauth/WaitingForOAuth.js b/src/views/oauth/WaitingForOAuth.js index 80ae02cc48..97864dd53f 100644 --- a/src/views/oauth/WaitingForOAuth.js +++ b/src/views/oauth/WaitingForOAuth.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import PropTypes from 'prop-types' import { of, defer } from 'rxjs' -import { map, mergeMap, delay, pluck } from 'rxjs/operators' +import { map, mergeMap, pluck, first } from 'rxjs/operators' import { Text } from '@mxenabled/mxui' import { useTokens } from '@kyper/tokenprovider' @@ -39,6 +39,8 @@ export const WaitingForOAuth = ({ const getNextDelay = getDelay() const { api } = useApi() + const clientLocale = document.querySelector('html')?.getAttribute('lang') || 'en' + useEffect(() => { /** * This gets the most recent PENDING oauth state for the member and polls that @@ -55,7 +57,6 @@ export const WaitingForOAuth = ({ * the oauth state created and know which oauth state to retreive ahead of time. */ const oauthStateCompleted$ = of(member).pipe( - delay(1500), mergeMap(() => defer(() => api.loadOAuthStates({ @@ -66,15 +67,35 @@ export const WaitingForOAuth = ({ ), pluck(0), // get the first response. Should be sorted by newest first mergeMap((latestState) => pollOauthState(latestState.guid, api)), - map((pollingState) => { + mergeMap((pollingState) => { const oauthState = pollingState.currentResponse + const memberGuid = oauthState.inbound_member_guid + + if ( + oauthState.auth_status === OauthState.AuthStatus.SUCCESS && + memberGuid !== member.guid + ) { + /** + * If the inbound member guid is different from our current member guid, + * we need to fetch the new member's record so that we can sync it into + * our redux state. + */ + return defer(() => api.loadMemberByGuid(memberGuid, clientLocale)).pipe( + map((member) => ({ + error: false, + memberGuid, + member, + })), + ) + } - return { + return of({ error: oauthState.auth_status === OauthState.AuthStatus.ERRORED, errorReason: OauthState.ReadableErrorReason[oauthState.error_reason], - memberGuid: oauthState.inbound_member_guid, - } + memberGuid, + }) }), + first(), ) /** @@ -84,7 +105,7 @@ export const WaitingForOAuth = ({ const sub$ = oauthStateCompleted$.subscribe( (resp) => { if (!resp.error) { - onOAuthSuccess(resp.memberGuid) + onOAuthSuccess(resp.memberGuid, resp.member) } else { onOAuthError(resp.memberGuid, resp.errorReason) } diff --git a/src/views/oauth/__tests__/OAuthStep-test.tsx b/src/views/oauth/__tests__/OAuthStep-test.tsx index 2caf4e9c10..c70460582c 100644 --- a/src/views/oauth/__tests__/OAuthStep-test.tsx +++ b/src/views/oauth/__tests__/OAuthStep-test.tsx @@ -3,6 +3,7 @@ import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { OAuthStep } from 'src/views/oauth/OAuthStep' import { apiValue } from 'src/const/apiProviderMock' import { ApiProvider } from 'src/context/ApiContext' +import { OAUTH_STATE } from 'src/services/mockedData' describe('OauthStep view', () => { describe('Ensure OAuthDefault is rendered', () => { @@ -21,6 +22,7 @@ describe('OauthStep view', () => { connect: { members: [], currentMemberGuid: null, + location: [{ step: 'SEARCH' }], }, }, }, @@ -32,16 +34,81 @@ describe('OauthStep view', () => { await user.click(loginButton) const tryAgainButton = await screen.findByRole('button', { name: 'Try again' }) expect(tryAgainButton).toBeInTheDocument() - waitFor( + await waitFor( async () => { expect(tryAgainButton).not.toBeDisabled() await user.click(tryAgainButton) }, - { timeout: 2500 }, + { timeout: 5000 }, ) - waitFor(() => { - expect(loginButton).toBeInTheDocument() + await screen.findByTestId('continue-button') + }) + + it('should prioritize existing member for OAuth URI generation when currentMemberGuid is present', async () => { + const getOAuthWindowURISpy = vi.spyOn(apiValue, 'getOAuthWindowURI') + const addMemberSpy = vi.spyOn(apiValue, 'addMember') + + const existingMember = { + guid: 'MBR-EXISTING', + institution_guid: 'INS-123', + is_oauth: true, + } + + render( + + + , + { + preloadedState: { + connect: { + members: [existingMember], + currentMemberGuid: 'MBR-EXISTING', + location: [{ step: 'SEARCH' }], + }, + }, + }, + ) + + await waitFor(() => { + expect(getOAuthWindowURISpy).toHaveBeenCalledWith('MBR-EXISTING', expect.anything()) }) + expect(addMemberSpy).not.toHaveBeenCalled() }) + + it('should update Redux state when handleOAuthSuccess is called with a member object from WaitingForOAuth', async () => { + const loadOAuthStates = () => + Promise.resolve([{ ...OAUTH_STATE.oauth_state, guid: 'OAS-123', auth_status: 1 }]) + const loadOAuthState = () => + Promise.resolve({ + ...OAUTH_STATE.oauth_state, + guid: 'OAS-123', + auth_status: 2, + inbound_member_guid: 'MBR-NEW', + }) + const loadMemberByGuid = () => Promise.resolve({ guid: 'MBR-NEW', name: 'New Member' }) + + const { user, store } = render(, { + apiValue: { ...apiValue, loadOAuthStates, loadOAuthState, loadMemberByGuid }, + preloadedState: { + connect: { + members: [], + currentMemberGuid: null, + location: [{ step: 'SEARCH' }], + }, + }, + }) + + const loginButton = await screen.findByTestId('continue-button') + await user.click(loginButton) + + await waitFor( + () => { + const state = store.getState() + expect(state.connect.currentMemberGuid).toBe('MBR-NEW') + expect(state.connect.members).toContainEqual({ guid: 'MBR-NEW', name: 'New Member' }) + }, + { timeout: 30000 }, + ) + }, 35000) }) }) diff --git a/src/views/oauth/__tests__/WaitingForOAuth-test.tsx b/src/views/oauth/__tests__/WaitingForOAuth-test.tsx index 1e72a4e691..5c09f908bb 100644 --- a/src/views/oauth/__tests__/WaitingForOAuth-test.tsx +++ b/src/views/oauth/__tests__/WaitingForOAuth-test.tsx @@ -69,5 +69,31 @@ describe('WaitingForOAuth view', () => { { timeout: 3000 }, ) }) + + it('should call api.loadMemberByGuid and onOAuthSuccess with member if the inbound_member_guid differs from the current member guid', async () => { + const loadOAuthState = () => + Promise.resolve({ + ...OAUTH_STATE.oauth_state, + auth_status: 2, + inbound_member_guid: 'MBR-NEW', + }) + const loadMemberByGuidSpy = vi + .spyOn(apiValue, 'loadMemberByGuid') + .mockResolvedValue({ guid: 'MBR-NEW' }) + + render( + + + , + ) + + await waitFor( + async () => { + expect(loadMemberByGuidSpy).toHaveBeenCalledWith('MBR-NEW', expect.anything()) + expect(defaultProps.onOAuthSuccess).toHaveBeenCalledWith('MBR-NEW', { guid: 'MBR-NEW' }) + }, + { timeout: 3000 }, + ) + }) }) }) diff --git a/tmp/planning/CTT-178/CTT-178-refined.md b/tmp/planning/CTT-178/CTT-178-refined.md new file mode 100644 index 0000000000..cea5ee92ee --- /dev/null +++ b/tmp/planning/CTT-178/CTT-178-refined.md @@ -0,0 +1,36 @@ +# Refined Jira Ticket: CTT-178 + +## Purpose Statement + +To reintroduce the OAuth member update flow within the Connect Widget. The removal of this flow (originally intended to prevent member combination issues) broke the critical migration path from non-OAuth to OAuth connections, resulting in duplicate members. This refinement ensures that users can successfully update existing members during non-OAuth to OAuth migrations, while also enabling the widget to gracefully handle cases where the backend provides a different member GUID than the one initially targeted for update (coordinated with backend fix DC-3040). + +## User Stories + +- **As a user migrating to an OAuth-enabled institution**, I want to be able to update my existing non-OAuth member so that I can maintain my transaction history and avoid creating a duplicate member. +- **As a developer**, I want the Connect Widget to robustly handle changes in member GUIDs during the OAuth connection phase, so that the application state remains synchronized with the backend regardless of whether the update resulted in a new or existing member record. + +## Acceptance Criteria + +- [ ] **Flow Reintroduction**: Restore the capability to initiate an "update" flow for members using OAuth. +- [ ] **Dynamic Member Synchronization**: Update the OAuth callback handling logic to upsert the `incoming_member_id` into redux state. +- [ ] **State Reconciliation**: If the `incoming_member_id` received from the OAuth state is not currently in the Redux store, the widget must fetch the new member record and integrate it into the active session state. +- [ ] **Error Handling**: Gracefully handle scenarios where fetching the "new" member GUID fails after a successful OAuth return. +- [ ] **Test Coverage**: Add unit or integration tests (using Vitest/MSW) that simulate: + - A successful OAuth update where the GUID remains the same. + - A successful OAuth update where the backend returns a _different_ GUID (migration/separation scenario). +- [ ] **Documentation**: Ensure all repository documentation, including READMEs, are updated to reflect the reintroduced flow and the logic for handling dynamic member IDs. + +## Out of Scope + +- **Backend Implementation**: The changes required in Firefly (handled via DC-3040) are external to this ticket. +- **Legacy MBR Combination Logic**: We are not reverting to the previous flawed logic; we are implementing a new synchronization pattern. +- **End-to-End OAuth Automation**: Improvements to the actual E2E testing infrastructure for real OAuth redirects (unless manageable via current mocks). + +## Definition of Done + +- [ ] Logic implemented for OAuth update and dynamic GUID synchronization. +- [ ] All new logic covered by unit/integration tests. +- [ ] Repository documentation (README, internal guides) updated. +- [ ] Pull Request follows Conventional Commit standards. +- [ ] PR reviewed and approved by the tech lead or designated peer. +- [ ] Verified that no regressions were introduced to the "Create Member" OAuth flow. diff --git a/tmp/planning/CTT-178/CTT-178.md b/tmp/planning/CTT-178/CTT-178.md new file mode 100644 index 0000000000..46bc50f883 --- /dev/null +++ b/tmp/planning/CTT-178/CTT-178.md @@ -0,0 +1,44 @@ +# Jira Ticket: CTT-178 + +## Summary + +npm | OAuth update flow | Reintroduce the update flow without reintroducing MBR combination problems + +## Description + +In the past, _we removed the ability for OAuth members to be updated_ via the widget, which resolved a problem where two separate logins to an institution get combined into one member. Since we always create a new member, it always resolves to a new member when new credentials are used, solving the problem. + +What was silently introduced as a problem, is this breaks how member migration from non-oauth to oauth work. When members get migrated from non-oauth to oauth, users need to be able to update their existing member to keep their data. In summary, _when a user isn’t able to update an oauth member, it ends up as a duplicate member instead during migrations to oauth_. + +This is a core recurring event that needs to work reliably for MX + +## Prerequisite changes that are required to fix the issue + +- The backend, Firefly, needs to be adjusted to handle the scenario when a user attempts to update an existing OAuth member and the user happens to use a separate login at the same bank than the member current has saved. @Reed Allred and @Jeff Johnson are handling this in https://mxcom.atlassian.net/browse/DC-3040 + +## GitHub npm package changes + +- (GitHub) Reintroduce the ability to update an OAuth member +- (GitHub) Ensure the widget can handle a brand new member it has never seen. When the `incoming_member_id` from the OAuth state is not known to the widget, it should be able to get the new record into its state +- Tests - add any tests that we can surrounding this flow + +## E2E tests to consider (can we get around our current oauth testing limitations?) + +- (Connect Widget and/or Firefly) Ensure logins aren’t combined into one member when different oauth logins are used. +- (Connect Widget and/or Firefly) Ensure that new members coming in from OAuth are saved into redux correctly. When the user attempts to update an OAuth member, but signs in with a separate login, the backend will provide the widget a brand new `inbound_member_guid` that is has never seen. This member should be fetched and saved into redux state. + +## Parent Issue + +CTT-177: OAuth update flow | Reintroduce the update flow without reintroducing MBR combination problems + +## Status + +In Progress + +## Priority + +Medium + +## Assignee + +Logan Rasmussen diff --git a/tmp/planning/CTT-178/baseline.txt b/tmp/planning/CTT-178/baseline.txt new file mode 100644 index 0000000000..a8d04c05f6 --- /dev/null +++ b/tmp/planning/CTT-178/baseline.txt @@ -0,0 +1,238 @@ + +> @mxenabled/connect-widget@0.0.0-semantic-release test +> vitest run + + + RUN v3.2.4 /Users/logan.rasmussen/dev/github-connect-widget + + ✓ src/redux/reducers/__tests__/config-test.js (19 tests) 10ms + ✓ src/utilities/__tests__/institutionStatus-test.tsx (16 tests) 34ms + ✓ src/utilities/transport/__tests__/MemberUpdateTransport-test.ts (10 tests) 15ms + ✓ src/redux/reducers/__tests__/Connect-test.js (86 tests) 10ms + ✓ src/utilities/__tests__/JobSchedule-test.js (15 tests) 3ms + ✓ src/utilities/__tests__/pollers-test.js (9 tests) 5ms + ✓ src/views/actionableError/__tests__/useActionableErrorMap-test.tsx (6 tests) 31ms + ✓ src/hooks/__tests__/useLoadConnect-test.tsx (13 tests) 152ms + ✓ src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx (5 tests) 97ms + ✓ src/views/verification/__tests__/VerifyExistingMember-test.tsx (5 tests) 200ms + ✓ src/views/oauth/experiments/__tests__/PredirectInstructions-test.tsx (5 tests) 155ms +stderr | src/views/credentials/__tests__/Credentials-test.tsx > Credentials > clicks the go to website and goes to website directly +Error: Not implemented: window.open + at module.exports (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17) + at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/Window.js:962:7 + at goToUrlLink (/Users/logan.rasmussen/dev/github-connect-widget/src/utilities/global.js:17:20) + at onClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/credentials/Credentials.js:196:17) + at executeDispatch (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) + at runWithFiberInDEV (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:874:13) + at processDispatchQueue (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19166:19) + at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 + at batchedUpdates$1 (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:3255:40) + at dispatchEventForPluginEventSystem (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19320:7) undefined + + ✓ src/views/credentials/__tests__/Credentials-test.tsx (7 tests) 636ms + ✓ Credentials > renders credentials, enters username and password 453ms + ✓ src/utilities/__tests__/PostMessage-test.js (9 tests) 3ms + ✓ src/utilities/__tests__/Reducer-test.js (10 tests) 5ms + ✓ src/views/search/__tests__/Search-test.js (13 tests) 1329ms + ✓ Search View > Search component > searches for institutions and renders the result 624ms + ✓ Search View > Search component > returns "No results found" if a bogus search term is used 573ms + ✓ src/utilities/__tests__/UserFeatures-test.js (8 tests) 6ms +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > shows SharedRoutingNumber when IAV_PREFERRED and institutions are found + DEPRECATED - use + +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > shows SharedRoutingNumber when IAV_PREFERRED and institutions are found + DEPRECATED - use + +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > calls loadInstitutions with include_identity flag when enabled + DEPRECATED - use + +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > calls loadInstitutions with include_identity flag when enabled + DEPRECATED - use + + ✓ src/redux/actions/__tests__/Connect-test.js (9 tests) 5ms +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Navigation > renders SharedRoutingNumber component when institutions are found + DEPRECATED - use + +stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Navigation > renders SharedRoutingNumber component when institutions are found + DEPRECATED - use + + ✓ src/utilities/__tests__/validation-test.js (7 tests) 3ms + ✓ src/views/microdeposits/__tests__/RoutingNumber-test.js (31 tests) 2793ms + ✓ src/views/actionableError/__tests__/ActionableError-test.tsx (5 tests) 126ms + ✓ src/views/loginError/__tests__/SecondaryAction-test.tsx (7 tests) 166ms +stderr | src/views/additionalProduct/__tests__/AdditionalProductStep-test.tsx > AdditionalProductStep - Account verification > should render the add account_verification text and handle yes click +The result function returned its own inputs without modification. e.g +`createSelector([state => state.todos], todos => todos)` +This could lead to inefficient memoization and unnecessary re-renders. +Ensure transformation logic is in the result function, and extraction logic is in the input selectors. { + stack: 'Error: \n' + + ' at Object.runIdentityFunctionCheck [as run] (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/devModeChecks/identityFunctionCheck.ts:39:15)\n' + + ' at dependenciesChecker (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/createSelectorCreator.ts:418:33)\n' + + ' at memoized (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/weakMapMemoize.ts:228:21)\n' + + ' at memoized (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-redux/src/hooks/useSelector.ts:171:28)\n' + + ' at memoizedSelector (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:46:30)\n' + + ' at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:70:22\n' + + ' at mountSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:8125:24)\n' + + ' at Object.useSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:26269:16)\n' + + ' at process.env.NODE_ENV.exports.useSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react/cjs/react.development.js:1270:34)\n' + + ' at process.env.NODE_ENV.exports.useSyncExternalStoreWithSelector (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:81:19)' +} + + ✓ src/views/additionalProduct/__tests__/AdditionalProductStep-test.tsx (2 tests) 228ms +stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > renders accessibility and footer elements +An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > renders correctly with both OAuth and non-OAuth members +An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > exposes correct imperative handle methods +An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > handles edge cases gracefully +An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + + ✓ src/views/connected/__tests__/Connected-test.tsx (7 tests) 855ms + ✓ Connected > handles done button click correctly 574ms + ✓ src/views/connecting/__tests__/Connecting-test.tsx (11 tests) 6350ms + ✓ > memberStatusUpdate > fires the override memberStatusUpdated event if it is provided 3106ms + ✓ > memberStatusUpdate > fires the default memberStatusUpdated event 3044ms + ✓ src/utilities/__tests__/global-test.js (7 tests) 3ms + ✓ src/hooks/__tests__/useAnalyticsPath-test.tsx (4 tests) 108ms + ✓ src/views/oauth/__tests__/OAuthError-test.tsx (7 tests) 170ms + ✓ src/components/__tests__/ConnectSuccessSurvey-test.tsx (4 tests) 324ms + ✓ src/utilities/__tests__/Personalization-test.js (8 tests) 2ms + ✓ src/components/app/__tests__/TooSmallDialog-test.tsx (5 tests) 114ms + ✓ src/views/disclosure/__tests__/DataRequested-test.tsx (4 tests) 69ms + ✓ src/views/demoConnectGuard/DemoConnectGuard-test.tsx (2 tests) 170ms + ✓ src/components/__tests__/InstitutionTile-test.jsx (6 tests) 65ms + ✓ src/views/oauth/experiments/__tests__/predirectInstructionUtils-test.ts (5 tests) 2ms + ✓ src/utilities/__tests__/reduxHelpers-test.js (3 tests) 8ms + ✓ src/components/__tests__/InstitutionBlock-test.tsx (6 tests) 43ms + ✓ src/__tests__/ConnectWidget-test.tsx (1 test) 664ms + ✓ ConnectWidget > renders the real Connect widget and handles WebSocket messages correctly 664ms + ✓ src/components/__tests__/ConnectLogoHeader-test.tsx (4 tests) 45ms + ✓ src/views/loginError/__tests__/LoginError-test.jsx (2 tests) 70ms + ✓ src/views/verification/__tests__/VerifyError-test.tsx (5 tests) 115ms + ✓ src/utilities/__tests__/Browser-test.js (6 tests) 3ms + ✓ src/redux/selectors/__tests__/Connect-test.js (4 tests) 3ms + ✓ src/views/disclosure/__tests__/PrivacyPolicy-test.tsx (3 tests) 163ms + ✓ src/redux/selectors/__tests__/ClientColorScheme-test.js (3 tests) 4ms + ✓ src/views/oauth/__tests__/OAuthDefault-test.tsx (1 test) 91ms + ✓ src/hooks/__tests__/useNavigationPostMessage-test.tsx (2 tests) 17ms + ✓ src/hooks/__tests__/useAnalyticsEvent-test.tsx (2 tests) 67ms + ✓ src/utilities/__tests__/ActionHelpers-test.js (3 tests) 4ms + ✓ src/views/oauth/__tests__/WaitingForOAuth-test.tsx (4 tests) 7176ms + ✓ WaitingForOAuth view > Button delay for try again > should enable the tryAgain button after 2 seconds and call onOAuthRetry when clicked 2028ms + ✓ WaitingForOAuth view > Button delay for try again > should call onOAuthSuccess if polling an oauth state was successful 2514ms + ✓ WaitingForOAuth view > Button delay for try again > should call onOAuthError if polling an oauth state was unsuccessful 2528ms + ✓ src/utilities/__tests__/FormatUrl-test.js (8 tests) 3ms + ✓ src/utilities/__tests__/memberUtils-test.js (2 tests) 2ms + ✓ src/components/__tests__/RenderConnectStep-test.jsx (1 test) 128ms + ✓ src/components/__tests__/ConnectNavigationHeader-test.jsx (3 tests) 79ms +stderr | src/views/oauth/__tests__/OAuthStep-test.tsx > OauthStep view > Ensure OAuthDefault is rendered > should go back to Oauth Default when Try Again button is clicked on the waitingForOAuth screen +Error: Not implemented: window.open + at module.exports (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17) + at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/Window.js:962:7 + at Object.onSignInClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/oauth/OAuthStep.js:201:36) + at onClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/oauth/OAuthDefault.js:102:19) + at executeDispatch (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) + at runWithFiberInDEV (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:874:13) + at processDispatchQueue (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19166:19) + at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 + at batchedUpdates$1 (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:3255:40) + at dispatchEventForPluginEventSystem (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19320:7) undefined + + ✓ src/views/oauth/__tests__/OAuthStep-test.tsx (1 test) 168ms + ✓ src/views/disclosure/__tests__/Interstitial-test.tsx (2 tests) 199ms + ✓ src/components/__tests__/MemberError-test.tsx (3 tests) 34ms + ✓ src/redux/reducers/__tests__/experimentalFeaturesSlice-test.ts (3 tests) 3ms + ✓ src/utilities/__tests__/Intl-test.tsx (4 tests) 27ms + ✓ src/components/__tests__/InstitutionGridTile-test.jsx (2 tests) 32ms + ✓ src/components/support/__tests__/GeneralSupport-test.tsx (2 tests) 1444ms + ✓ GeneralSupport > renders generalSupport ticket 1091ms + ✓ GeneralSupport > renders generalSupport ticket and cancels 352ms + ✓ src/utilities/__tests__/timeoutHelper-test.js (5 tests) 2ms + ✓ src/views/mfa/__tests__/MFAStep-test.tsx (2 tests) 190ms + ✓ src/components/support/__tests__/Support-test.tsx (2 tests) 148ms + ✓ src/views/consent/__tests__/DynamicDisclosure-test.tsx (2 tests) 334ms + ✓ src/redux/actions/__tests__/browser-test.js (2 tests) 2ms + ✓ src/redux/__tests__/Store-test.js (5 tests) 2ms + ✓ src/hooks/__tests__/useSelectInstitution-test.tsx (1 test) 112ms + ✓ src/components/__tests__/DayOfMonthPicker-test.tsx (2 tests) 118ms + ✓ src/redux/reducers/__tests__/userFeaturesSlice-test.js (3 tests) 3ms + ✓ src/views/mfa/__tests__/utils-test.js (5 tests) 1ms + ✓ src/utilities/__tests__/Accessibility-test.js (3 tests) 3ms + ✓ src/redux/reducers/__tests__/app-test.js (2 tests) 2ms + ✓ src/redux/actions/__tests__/app-test.js (1 test) 2ms + ✓ src/components/support/__tests__/SupportMenu-test.tsx (2 tests) 71ms + ✓ src/components/__tests__/ClientLogo-test.tsx (2 tests) 18ms + ✓ src/components/__tests__/GoBackButton-test.tsx (3 tests) 59ms + ✓ src/views/manualAccount/__tests__/manualAccountMenu-test.tsx (1 test) 90ms + ✓ src/components/__tests__/AriaLive-test.tsx (2 tests) 18ms + ✓ src/views/disclosure/__tests__/DataAvailable-test.tsx (1 test) 25ms + ✓ src/components/support/__tests__/SupportSuccess-test.tsx (1 test) 435ms + ✓ SupportSuccess > renders and closes when Continue is clicked 434ms + ✓ src/hooks/__tests__/usePollMember-test.tsx (20 tests) 45305ms + ✓ usePollMember > should poll successfully and emit polling states with member and job data 3007ms + ✓ usePollMember > should handle API context not being available 3019ms + ✓ usePollMember > should handle loadMemberByGuid errors gracefully 3025ms + ✓ usePollMember > should handle loadJob errors gracefully 3033ms + ✓ usePollMember > should set initialDataReady when async_account_data_ready is true 3022ms + ✓ usePollMember > should not set initialDataReady when optOutOfEarlyUserRelease is true 3020ms + ✓ usePollMember > should use custom polling interval when provided 513ms + ✓ usePollMember > should increment pollingCount on each poll 2050ms + ✓ usePollMember > should show CHALLENGED status message when member is challenged 3022ms + ✓ usePollMember > should continue polling when member is still being aggregated 3024ms + ✓ usePollMember > should use client locale from html lang attribute 3021ms + ✓ usePollMember > should only set initialDataReady once, even on subsequent polls 2408ms + ✓ usePollMember > should not set initialDataReady when async_account_data_ready is false 3009ms + ✓ usePollMember > should not set initialDataReady when there is an error 3012ms + ✓ usePollMember > should set initialDataReady when async_account_data_ready becomes true after being false 2040ms + ✓ usePollMember > should correctly update previousResponse and currentResponse over multiple polls 2043ms + ✓ usePollMember > should preserve previousResponse and currentResponse when an intermediate poll fails 3012ms + + Test Files 81 passed (81) + Tests 514 passed (514) + Start at 13:57:38 + Duration 46.56s (transform 2.24s, setup 10.30s, collect 248.50s, tests 71.77s, environment 32.83s, prepare 4.75s) + + +> @mxenabled/connect-widget@0.0.0-semantic-release lint +> eslint . --ext ts,tsx,js,jsx,md --report-unused-disable-directives --max-warnings 14 + diff --git a/tmp/planning/CTT-178/plan.md b/tmp/planning/CTT-178/plan.md new file mode 100644 index 0000000000..938a16e8b1 --- /dev/null +++ b/tmp/planning/CTT-178/plan.md @@ -0,0 +1,180 @@ +# Implementation Plan: CTT-178 - Restore OAuth Member Update Flow and Dynamic GUID Synchronization + +## 1. Analysis Summary + +- **Type:** New Feature / Architectural Initiative (Reintroduction of flow) +- **Complexity:** Medium +- **Impact:** Critical for non-OAuth to OAuth migrations; ensures session integrity when member GUIDs change during OAuth. +- **Confidence Level:** High - The proposed solution leverages existing Redux actions and RxJS patterns within the widget. + +## 2. Affected Code Areas + +### Primary Files + +- `src/views/oauth/OAuthStep.js` - Re-enable update flow and handle fetched member data. +- `src/views/oauth/WaitingForOAuth.js` - Implement dynamic member GUID detection and fetching. + +### New Files + +- None. + +## 3. Solution Architecture + +```mermaid +sequenceDiagram + participant User + participant OAuthStep + participant API + participant WaitingForOAuth + participant Redux + participant Connecting + + User->>OAuthStep: Select Member for Update + OAuthStep->>API: getOAuthWindowURI(member.guid) + API-->>OAuthStep: oauth_window_uri + OAuthStep->>User: Redirect/Open OAuth Window + User->>WaitingForOAuth: Returns from OAuth + WaitingForOAuth->>API: loadOAuthStates(outbound_member_guid) + API-->>WaitingForOAuth: { inbound_member_guid: 'NEW-MBR' } + Note over WaitingForOAuth: Detected GUID change! + WaitingForOAuth->>API: loadMemberByGuid('NEW-MBR') + API-->>WaitingForOAuth: { member: { guid: 'NEW-MBR', ... } } + WaitingForOAuth->>OAuthStep: onOAuthSuccess('NEW-MBR', member) + OAuthStep->>Redux: dispatch(updateMemberSuccess(member)) + Redux-->>Connecting: state.currentMemberGuid = 'NEW-MBR' + Connecting->>API: pollMember('NEW-MBR') +``` + +## 4. Implementation Plan + +### Phase 1: Restore OAuth Update Flow + +1. **Modify `src/views/oauth/OAuthStep.js`**: Update the `useEffect` that starts the OAuth flow to check for an existing `member` from Redux state if no `pendingOauthMember` is found. +2. **Re-enable `getOAuthWindowURI`**: Ensure that if `member.guid` exists, the flow proceeds to fetch the OAuth URI instead of defaulting to `api.addMember`. + [Validation Checkpoint: Start Connect with `current_member_guid` for an OAuth-compatible institution. Verify it lands on `OAuthStep` and successfully generates the URI for that member.] + +### Phase 2: Dynamic Member Synchronization + +1. **Update `src/views/oauth/WaitingForOAuth.js`**: + - Modify the `oauthStateCompleted$` stream. + - After polling succeeds, check if `inbound_member_guid` differs from the initial `member.guid`. + - If different, use `api.loadMemberByGuid` to fetch the updated member record. + - Update the response object to include the fetched `member`. +2. **Error Handling**: If `loadMemberByGuid` fails, return an error state to trigger the OAuth error view. + [Validation Checkpoint: Mock `loadOAuthStates` to return a different `inbound_member_guid`. Verify that `loadMemberByGuid` is called with the new GUID.] + +### Phase 3: State Reconciliation + +1. **Update `OAuthStep.js` callbacks**: + - Update `handleOAuthSuccess` to accept `(memberGuid, member = null)`. + - If `member` is provided, dispatch `connectActions.updateMemberSuccess({ item: member })`. + - This ensures the Redux store has the new member and `currentMemberGuid` is updated before moving to the `CONNECTING` step. + [Validation Checkpoint: Complete an OAuth flow where the GUID changes. Verify that the `Connecting` view shows the correct institution name and begins polling for the NEW member GUID.] + +## 5. Proposed Code Changes (The Spike) + +### `src/views/oauth/OAuthStep.js` + +```diff +<<<< + if (pendingOauthMember) { + // If there is a pending oauth member, don't create a new one, use that one + member$ = of(pendingOauthMember) + } else { +==== + if (member && member.guid) { + // Use the currently active member (supports Update flows and re-using a member created in this session) + member$ = of(member) + } else if (pendingOauthMember) { + // If no active member, look for a pending oauth member for this institution + member$ = of(pendingOauthMember) + } else { +>>>> +``` + +```diff +<<<< + function handleOAuthSuccess(memberGuid) { + closeOAuthWindow() + dispatch(connectActions.handleOAuthSuccess(memberGuid)) + } +==== + function handleOAuthSuccess(memberGuid, member = null) { + closeOAuthWindow() + if (member) { + dispatch(connectActions.updateMemberSuccess(member)) + } + dispatch(connectActions.handleOAuthSuccess(memberGuid)) + } +>>>> +``` + +### `src/views/oauth/WaitingForOAuth.js` + +```diff +<<<< + map((pollingState) => { + const oauthState = pollingState.currentResponse + + return { + error: oauthState.auth_status === OauthState.AuthStatus.ERRORED, + errorReason: OauthState.ReadableErrorReason[oauthState.error_reason], + memberGuid: oauthState.inbound_member_guid, + } + }), +==== + mergeMap((pollingState) => { + const oauthState = pollingState.currentResponse + const inboundMemberGuid = oauthState.inbound_member_guid + + if (oauthState.auth_status === OauthState.AuthStatus.ERRORED) { + return of({ + error: true, + errorReason: OauthState.ReadableErrorReason[oauthState.error_reason], + memberGuid: inboundMemberGuid || member.guid, + }) + } + + if (inboundMemberGuid && inboundMemberGuid !== member.guid) { + return defer(() => api.loadMemberByGuid(inboundMemberGuid, clientLocale)).pipe( + map((resp) => ({ + error: false, + memberGuid: inboundMemberGuid, + member: resp.member, + })), + catchError(() => + of({ + error: true, + errorReason: 'Failed to fetch updated member information', + memberGuid: inboundMemberGuid, + }), + ), + ) + } + + return of({ + error: false, + memberGuid: inboundMemberGuid || member.guid, + }) + }), +>>>> +``` + +## 6. Validation & Testing + +- [ ] **Unit Tests:** + - `OAuthStep-test.js`: Verify update flow re-activation. + - `WaitingForOAuth-test.js`: Verify dynamic GUID detection and member fetching. +- [ ] **Integration Tests:** Use MSW to simulate a migration scenario where GUID changes from `MBR-OLD` to `MBR-NEW`. +- [ ] **Acceptance Criteria:** + - OAuth update flow is restored. + - State is synchronized with `incoming_member_id`. + - Redux store correctly integrates new members. + +## 7. Risk Mitigation + +- **Risk:** Mangled credentials if updating an existing OAuth member. -> **Mitigation:** The re-enabled flow is specifically for migrations and updates where the backend (Firefly DC-3040) is now handling the credential/member logic safely. +- **Risk:** Infinite polling if new member GUID is invalid. -> **Mitigation:** The fetch step for the new member ensures it exists before proceeding to `Connecting`. +- **Rollback Plan:** Revert changes to `OAuthStep.js` to restore the "always create new member" behavior. + +**Next Step**: Run `/planning:breakdown-plan CTT-178` to break this plan down into actionable tasks. diff --git a/tmp/planning/CTT-178/tasks.md b/tmp/planning/CTT-178/tasks.md new file mode 100644 index 0000000000..6f91c99969 --- /dev/null +++ b/tmp/planning/CTT-178/tasks.md @@ -0,0 +1,53 @@ +## Objective & Context + +Restore the OAuth member update flow and implement dynamic GUID synchronization in the Connect Widget. This ensures that users migrating from non-OAuth to OAuth connections can update their existing members correctly, even if the backend returns a different member GUID during the process (e.g., in migration or separation scenarios). + +## Tasks + +- [ ] 0.0 Setup Environment + - [x] 0.1 Verify or create development branch `feature/CTT-178` +- [x] 1.0 Establish Quality Baseline + - [x] 1.1 Run tests and linters to capture baseline metrics and save reports to `tmp/planning/CTT-178/baseline.txt` + +### Implementation Tasks + +- [ ] 2.0 Restore OAuth Update Initiation Flow (Agent: `General Senior Software Agent`) + - [x] 2.1 RED: Update `src/views/oauth/__tests__/OAuthStep-test.tsx` to verify that if an existing member GUID is present, it is prioritized for OAuth URI generation. + - [x] 2.2 GREEN: Modify `src/views/oauth/OAuthStep.js` to check for `member.guid` before `pendingOauthMember` in the OAuth initialization `useEffect`. + - [x] 2.3 REFACTOR: Ensure the conditional logic for URI generation is clear and adheres to project standards. + - [x] 2.4 CLEANUP: Run linter and verify `OAuthStep-test.tsx` passes. + - [x] 2.5 Create git checkpoint: `feat(oauth): prioritize existing member for update flow` + +- [ ] 3.0 Implement Dynamic GUID Synchronization Logic (Agent: `General Senior Software Agent`) + - [x] 3.1 RED: Update `src/views/oauth/__tests__/WaitingForOAuth-test.tsx` to simulate a scenario where `inbound_member_guid` differs from the current member GUID and verify `api.loadMemberByGuid` is called. + - [x] 3.2 GREEN: Modify the `oauthStateCompleted$` stream in `src/views/oauth/WaitingForOAuth.js` to detect GUID changes and fetch the new member record using `api.loadMemberByGuid`. + - [x] 3.3 REFACTOR: Optimize the RxJS pipeline (using `mergeMap` and `defer`) and ensure robust error handling for the member fetch. + - [x] 3.4 CLEANUP: Run linter and verify `WaitingForOAuth-test.tsx` passes. + - [x] 3.5 Create git checkpoint: `feat(oauth): implement dynamic member guid synchronization` + +- [ ] 4.0 Enhance Redux State Reconciliation (Agent: `General Senior Software Agent`) + - [x] 4.1 RED: Add a test case to `src/views/oauth/__tests__/OAuthStep-test.tsx` verifying that `handleOAuthSuccess` dispatches `updateMemberSuccess` when a member object is provided. + - [x] 4.2 GREEN: Update `handleOAuthSuccess` in `src/views/oauth/OAuthStep.js` to accept an optional `member` parameter and dispatch `connectActions.updateMemberSuccess` if present. + - [x] 4.3 REFACTOR: Ensure the callback signature and dispatch logic are clean and type-safe. + - [x] 4.4 CLEANUP: Run linter and verify all OAuth tests pass. + - [x] 4.5 Create git checkpoint: `feat(oauth): dispatch member update on successful sync` + +- [ ] 5.0 Verify Migration and Synchronization Scenarios (Agent: `General Senior Software Agent`) + - [x] 5.1 RED: Create or update an integration test (e.g., in `src/__tests__/ConnectWidget-test.tsx`) using `apiValue` mocking to simulate a full migration flow where the GUID changes from `MBR-OLD` to `MBR-NEW`. + - [x] 5.2 GREEN: Ensure the widget correctly transitions from `WaitingForOAuth` to `Connecting` with the new GUID and correct institution data. + - [x] 5.3 REFACTOR: Clean up mock data and test utilities used for the integration test. + - [x] 5.4 CLEANUP: Run full test suite and verify all integration tests pass. + - [x] 5.5 Create git checkpoint: `test(oauth): add integration test for guid migration scenario` + +- [x] 6.0 Verification & Regression Check (Agent: `General Senior Software Agent`) + - [x] 6.1 Run full test suite and verify no regressions were introduced to existing OAuth or non-OAuth flows. + - [x] 6.2 Run linters to verify code quality has not decreased compared to baseline. + +- [x] 7.0 Review and Update Documentation (Agent: `tech-lead`) + - [x] 7.1 DOCS: Update `docs/APIDOCUMENTATION.md` and `docs/USER_FEATURES.md` to reflect the reintroduced OAuth update flow and the dynamic GUID synchronization logic. + - [x] 7.2 Create git checkpoint: `docs(oauth): document update flow and guid synchronization` + +- [x] 8.0 Finalize Task + - [x] 8.1 Squash all checkpoint commits into a single clean conventional commit: `feat(oauth): restore update flow and implement dynamic guid sync` + +**Next Step**: Run `/swe:implement-plan CTT-178` to begin implementation. From ea2e70b9309f3b14428d58a1261e49c0c9f008d3 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 30 Apr 2026 15:43:42 -0600 Subject: [PATCH 2/5] fix(codereview): implement ai code review suggestions --- src/views/oauth/OAuthStep.js | 24 +++++++++--------------- src/views/oauth/WaitingForOAuth.js | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/views/oauth/OAuthStep.js b/src/views/oauth/OAuthStep.js index 665cf7d95d..4c4107e2c3 100644 --- a/src/views/oauth/OAuthStep.js +++ b/src/views/oauth/OAuthStep.js @@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, useImperativeHandle, useContext } f import PropTypes from 'prop-types' import { useSelector, useDispatch } from 'react-redux' import { defer, of } from 'rxjs' -import { mergeMap, map, pluck } from 'rxjs/operators' +import { mergeMap, map } from 'rxjs/operators' import { useApi } from 'src/context/ApiContext' import { ReadableStatuses } from 'src/const/Statuses' @@ -123,22 +123,16 @@ export const OAuthStep = React.forwardRef((props, navigationRef) => { let member$ /** - * WARNING: don't change this area without data to back up your changes + * NOTE: We are re-enabling the use of existing member GUIDs for OAuth flows (Update flow). * - * There has been a flip-flop of problems in this area, so this note is being written as a warning. - * Using existing OAuth members causes problems, because if a new set of credentials is used for - * an existing member, our system ends up in a bad state, where the old member gets mangled up with - * the new credentials. + * Historically, this caused "member combination" issues. We now mitigate this by + * detecting if the backend returns a different inbound_member_guid in WaitingForOAuth.js. + * If a change is detected, we fetch the new member record and update the Redux state + * accordingly, ensuring session integrity even if the GUID migrates during the flow. * - * We tried to reduce the amount of members created by re-using existing oauth members, but that caused - * a regression of a client reported bug, so we had to move this back to always creating new members, - * or using existing pending oauth members. - * - * Previous code attempt that was used to reduce member creation, but reintroduced the bug: - * if (member && member.is_oauth && api.getOAuthWindowURI) { - * member$ = of(member) + * The backend will ultimately decide when to send us back the same member guid, or a new one */ - if (member?.guid) { + if (member?.is_oauth && api.getOAuthWindowURI) { // If there is an existing member, don't create a new one, use that one (restores update flow) member$ = of(member) } else if (pendingOauthMember) { @@ -158,7 +152,7 @@ export const OAuthStep = React.forwardRef((props, navigationRef) => { config, ), ) - .pipe(pluck('member')) + .pipe(map((resp) => resp.member)) .subscribe( (member) => { sendAnalyticsEvent(AnalyticEvents.OAUTH_PENDING_MEMBER_CREATED, { diff --git a/src/views/oauth/WaitingForOAuth.js b/src/views/oauth/WaitingForOAuth.js index 97864dd53f..8020775aa4 100644 --- a/src/views/oauth/WaitingForOAuth.js +++ b/src/views/oauth/WaitingForOAuth.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import PropTypes from 'prop-types' import { of, defer } from 'rxjs' -import { map, mergeMap, pluck, first } from 'rxjs/operators' +import { map, mergeMap, first, filter, catchError } from 'rxjs/operators' import { Text } from '@mxenabled/mxui' import { useTokens } from '@kyper/tokenprovider' @@ -63,9 +63,12 @@ export const WaitingForOAuth = ({ outbound_member_guid: member.guid, auth_status: OauthState.AuthStatus.PENDING, }), + ).pipe( + map((states) => states?.[0]), + catchError(() => of(null)), ), ), - pluck(0), // get the first response. Should be sorted by newest first + filter((latestState) => !!latestState), mergeMap((latestState) => pollOauthState(latestState.guid, api)), mergeMap((pollingState) => { const oauthState = pollingState.currentResponse @@ -81,11 +84,18 @@ export const WaitingForOAuth = ({ * our redux state. */ return defer(() => api.loadMemberByGuid(memberGuid, clientLocale)).pipe( - map((member) => ({ + map((fetchedMember) => ({ error: false, memberGuid, - member, + member: fetchedMember, })), + catchError(() => + of({ + error: true, + errorReason: __('Failed to synchronize member data'), + memberGuid, + }), + ), ) } From 642da175c8a9dd3a2d575809446c83781525b6bd Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 30 Apr 2026 15:45:26 -0600 Subject: [PATCH 3/5] fix: remove files we do not want to track --- tmp/planning/CTT-178/CTT-178-refined.md | 36 ---- tmp/planning/CTT-178/CTT-178.md | 44 ----- tmp/planning/CTT-178/baseline.txt | 238 ------------------------ tmp/planning/CTT-178/plan.md | 180 ------------------ tmp/planning/CTT-178/tasks.md | 53 ------ 5 files changed, 551 deletions(-) delete mode 100644 tmp/planning/CTT-178/CTT-178-refined.md delete mode 100644 tmp/planning/CTT-178/CTT-178.md delete mode 100644 tmp/planning/CTT-178/baseline.txt delete mode 100644 tmp/planning/CTT-178/plan.md delete mode 100644 tmp/planning/CTT-178/tasks.md diff --git a/tmp/planning/CTT-178/CTT-178-refined.md b/tmp/planning/CTT-178/CTT-178-refined.md deleted file mode 100644 index cea5ee92ee..0000000000 --- a/tmp/planning/CTT-178/CTT-178-refined.md +++ /dev/null @@ -1,36 +0,0 @@ -# Refined Jira Ticket: CTT-178 - -## Purpose Statement - -To reintroduce the OAuth member update flow within the Connect Widget. The removal of this flow (originally intended to prevent member combination issues) broke the critical migration path from non-OAuth to OAuth connections, resulting in duplicate members. This refinement ensures that users can successfully update existing members during non-OAuth to OAuth migrations, while also enabling the widget to gracefully handle cases where the backend provides a different member GUID than the one initially targeted for update (coordinated with backend fix DC-3040). - -## User Stories - -- **As a user migrating to an OAuth-enabled institution**, I want to be able to update my existing non-OAuth member so that I can maintain my transaction history and avoid creating a duplicate member. -- **As a developer**, I want the Connect Widget to robustly handle changes in member GUIDs during the OAuth connection phase, so that the application state remains synchronized with the backend regardless of whether the update resulted in a new or existing member record. - -## Acceptance Criteria - -- [ ] **Flow Reintroduction**: Restore the capability to initiate an "update" flow for members using OAuth. -- [ ] **Dynamic Member Synchronization**: Update the OAuth callback handling logic to upsert the `incoming_member_id` into redux state. -- [ ] **State Reconciliation**: If the `incoming_member_id` received from the OAuth state is not currently in the Redux store, the widget must fetch the new member record and integrate it into the active session state. -- [ ] **Error Handling**: Gracefully handle scenarios where fetching the "new" member GUID fails after a successful OAuth return. -- [ ] **Test Coverage**: Add unit or integration tests (using Vitest/MSW) that simulate: - - A successful OAuth update where the GUID remains the same. - - A successful OAuth update where the backend returns a _different_ GUID (migration/separation scenario). -- [ ] **Documentation**: Ensure all repository documentation, including READMEs, are updated to reflect the reintroduced flow and the logic for handling dynamic member IDs. - -## Out of Scope - -- **Backend Implementation**: The changes required in Firefly (handled via DC-3040) are external to this ticket. -- **Legacy MBR Combination Logic**: We are not reverting to the previous flawed logic; we are implementing a new synchronization pattern. -- **End-to-End OAuth Automation**: Improvements to the actual E2E testing infrastructure for real OAuth redirects (unless manageable via current mocks). - -## Definition of Done - -- [ ] Logic implemented for OAuth update and dynamic GUID synchronization. -- [ ] All new logic covered by unit/integration tests. -- [ ] Repository documentation (README, internal guides) updated. -- [ ] Pull Request follows Conventional Commit standards. -- [ ] PR reviewed and approved by the tech lead or designated peer. -- [ ] Verified that no regressions were introduced to the "Create Member" OAuth flow. diff --git a/tmp/planning/CTT-178/CTT-178.md b/tmp/planning/CTT-178/CTT-178.md deleted file mode 100644 index 46bc50f883..0000000000 --- a/tmp/planning/CTT-178/CTT-178.md +++ /dev/null @@ -1,44 +0,0 @@ -# Jira Ticket: CTT-178 - -## Summary - -npm | OAuth update flow | Reintroduce the update flow without reintroducing MBR combination problems - -## Description - -In the past, _we removed the ability for OAuth members to be updated_ via the widget, which resolved a problem where two separate logins to an institution get combined into one member. Since we always create a new member, it always resolves to a new member when new credentials are used, solving the problem. - -What was silently introduced as a problem, is this breaks how member migration from non-oauth to oauth work. When members get migrated from non-oauth to oauth, users need to be able to update their existing member to keep their data. In summary, _when a user isn’t able to update an oauth member, it ends up as a duplicate member instead during migrations to oauth_. - -This is a core recurring event that needs to work reliably for MX - -## Prerequisite changes that are required to fix the issue - -- The backend, Firefly, needs to be adjusted to handle the scenario when a user attempts to update an existing OAuth member and the user happens to use a separate login at the same bank than the member current has saved. @Reed Allred and @Jeff Johnson are handling this in https://mxcom.atlassian.net/browse/DC-3040 - -## GitHub npm package changes - -- (GitHub) Reintroduce the ability to update an OAuth member -- (GitHub) Ensure the widget can handle a brand new member it has never seen. When the `incoming_member_id` from the OAuth state is not known to the widget, it should be able to get the new record into its state -- Tests - add any tests that we can surrounding this flow - -## E2E tests to consider (can we get around our current oauth testing limitations?) - -- (Connect Widget and/or Firefly) Ensure logins aren’t combined into one member when different oauth logins are used. -- (Connect Widget and/or Firefly) Ensure that new members coming in from OAuth are saved into redux correctly. When the user attempts to update an OAuth member, but signs in with a separate login, the backend will provide the widget a brand new `inbound_member_guid` that is has never seen. This member should be fetched and saved into redux state. - -## Parent Issue - -CTT-177: OAuth update flow | Reintroduce the update flow without reintroducing MBR combination problems - -## Status - -In Progress - -## Priority - -Medium - -## Assignee - -Logan Rasmussen diff --git a/tmp/planning/CTT-178/baseline.txt b/tmp/planning/CTT-178/baseline.txt deleted file mode 100644 index a8d04c05f6..0000000000 --- a/tmp/planning/CTT-178/baseline.txt +++ /dev/null @@ -1,238 +0,0 @@ - -> @mxenabled/connect-widget@0.0.0-semantic-release test -> vitest run - - - RUN v3.2.4 /Users/logan.rasmussen/dev/github-connect-widget - - ✓ src/redux/reducers/__tests__/config-test.js (19 tests) 10ms - ✓ src/utilities/__tests__/institutionStatus-test.tsx (16 tests) 34ms - ✓ src/utilities/transport/__tests__/MemberUpdateTransport-test.ts (10 tests) 15ms - ✓ src/redux/reducers/__tests__/Connect-test.js (86 tests) 10ms - ✓ src/utilities/__tests__/JobSchedule-test.js (15 tests) 3ms - ✓ src/utilities/__tests__/pollers-test.js (9 tests) 5ms - ✓ src/views/actionableError/__tests__/useActionableErrorMap-test.tsx (6 tests) 31ms - ✓ src/hooks/__tests__/useLoadConnect-test.tsx (13 tests) 152ms - ✓ src/views/institutionStatusDetails/__tests__/InstitutionStatusDetails-test.tsx (5 tests) 97ms - ✓ src/views/verification/__tests__/VerifyExistingMember-test.tsx (5 tests) 200ms - ✓ src/views/oauth/experiments/__tests__/PredirectInstructions-test.tsx (5 tests) 155ms -stderr | src/views/credentials/__tests__/Credentials-test.tsx > Credentials > clicks the go to website and goes to website directly -Error: Not implemented: window.open - at module.exports (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17) - at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/Window.js:962:7 - at goToUrlLink (/Users/logan.rasmussen/dev/github-connect-widget/src/utilities/global.js:17:20) - at onClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/credentials/Credentials.js:196:17) - at executeDispatch (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) - at runWithFiberInDEV (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:874:13) - at processDispatchQueue (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19166:19) - at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 - at batchedUpdates$1 (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:3255:40) - at dispatchEventForPluginEventSystem (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19320:7) undefined - - ✓ src/views/credentials/__tests__/Credentials-test.tsx (7 tests) 636ms - ✓ Credentials > renders credentials, enters username and password 453ms - ✓ src/utilities/__tests__/PostMessage-test.js (9 tests) 3ms - ✓ src/utilities/__tests__/Reducer-test.js (10 tests) 5ms - ✓ src/views/search/__tests__/Search-test.js (13 tests) 1329ms - ✓ Search View > Search component > searches for institutions and renders the result 624ms - ✓ Search View > Search component > returns "No results found" if a bogus search term is used 573ms - ✓ src/utilities/__tests__/UserFeatures-test.js (8 tests) 6ms -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > shows SharedRoutingNumber when IAV_PREFERRED and institutions are found - DEPRECATED - use - -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > shows SharedRoutingNumber when IAV_PREFERRED and institutions are found - DEPRECATED - use - -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > calls loadInstitutions with include_identity flag when enabled - DEPRECATED - use - -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Blocked Routing Number Cases > calls loadInstitutions with include_identity flag when enabled - DEPRECATED - use - - ✓ src/redux/actions/__tests__/Connect-test.js (9 tests) 5ms -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Navigation > renders SharedRoutingNumber component when institutions are found - DEPRECATED - use - -stderr | src/views/microdeposits/__tests__/RoutingNumber-test.js > RoutingNumber > Navigation > renders SharedRoutingNumber component when institutions are found - DEPRECATED - use - - ✓ src/utilities/__tests__/validation-test.js (7 tests) 3ms - ✓ src/views/microdeposits/__tests__/RoutingNumber-test.js (31 tests) 2793ms - ✓ src/views/actionableError/__tests__/ActionableError-test.tsx (5 tests) 126ms - ✓ src/views/loginError/__tests__/SecondaryAction-test.tsx (7 tests) 166ms -stderr | src/views/additionalProduct/__tests__/AdditionalProductStep-test.tsx > AdditionalProductStep - Account verification > should render the add account_verification text and handle yes click -The result function returned its own inputs without modification. e.g -`createSelector([state => state.todos], todos => todos)` -This could lead to inefficient memoization and unnecessary re-renders. -Ensure transformation logic is in the result function, and extraction logic is in the input selectors. { - stack: 'Error: \n' + - ' at Object.runIdentityFunctionCheck [as run] (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/devModeChecks/identityFunctionCheck.ts:39:15)\n' + - ' at dependenciesChecker (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/createSelectorCreator.ts:418:33)\n' + - ' at memoized (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/reselect/src/weakMapMemoize.ts:228:21)\n' + - ' at memoized (file:///Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-redux/src/hooks/useSelector.ts:171:28)\n' + - ' at memoizedSelector (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:46:30)\n' + - ' at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:70:22\n' + - ' at mountSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:8125:24)\n' + - ' at Object.useSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:26269:16)\n' + - ' at process.env.NODE_ENV.exports.useSyncExternalStore (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react/cjs/react.development.js:1270:34)\n' + - ' at process.env.NODE_ENV.exports.useSyncExternalStoreWithSelector (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/use-sync-external-store/cjs/use-sync-external-store-with-selector.development.js:81:19)' -} - - ✓ src/views/additionalProduct/__tests__/AdditionalProductStep-test.tsx (2 tests) 228ms -stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > renders accessibility and footer elements -An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). - -When testing, code that causes React state updates should be wrapped into act(...): - -act(() => { - /* fire events that update state */ -}); -/* assert on the output */ - -This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act - -stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > renders correctly with both OAuth and non-OAuth members -An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). - -When testing, code that causes React state updates should be wrapped into act(...): - -act(() => { - /* fire events that update state */ -}); -/* assert on the output */ - -This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act - -stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > exposes correct imperative handle methods -An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). - -When testing, code that causes React state updates should be wrapped into act(...): - -act(() => { - /* fire events that update state */ -}); -/* assert on the output */ - -This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act - -stderr | src/views/connected/__tests__/Connected-test.tsx > Connected > handles edge cases gracefully -An update to ForwardRef(TouchRipple) inside a test was not wrapped in act(...). - -When testing, code that causes React state updates should be wrapped into act(...): - -act(() => { - /* fire events that update state */ -}); -/* assert on the output */ - -This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act - - ✓ src/views/connected/__tests__/Connected-test.tsx (7 tests) 855ms - ✓ Connected > handles done button click correctly 574ms - ✓ src/views/connecting/__tests__/Connecting-test.tsx (11 tests) 6350ms - ✓ > memberStatusUpdate > fires the override memberStatusUpdated event if it is provided 3106ms - ✓ > memberStatusUpdate > fires the default memberStatusUpdated event 3044ms - ✓ src/utilities/__tests__/global-test.js (7 tests) 3ms - ✓ src/hooks/__tests__/useAnalyticsPath-test.tsx (4 tests) 108ms - ✓ src/views/oauth/__tests__/OAuthError-test.tsx (7 tests) 170ms - ✓ src/components/__tests__/ConnectSuccessSurvey-test.tsx (4 tests) 324ms - ✓ src/utilities/__tests__/Personalization-test.js (8 tests) 2ms - ✓ src/components/app/__tests__/TooSmallDialog-test.tsx (5 tests) 114ms - ✓ src/views/disclosure/__tests__/DataRequested-test.tsx (4 tests) 69ms - ✓ src/views/demoConnectGuard/DemoConnectGuard-test.tsx (2 tests) 170ms - ✓ src/components/__tests__/InstitutionTile-test.jsx (6 tests) 65ms - ✓ src/views/oauth/experiments/__tests__/predirectInstructionUtils-test.ts (5 tests) 2ms - ✓ src/utilities/__tests__/reduxHelpers-test.js (3 tests) 8ms - ✓ src/components/__tests__/InstitutionBlock-test.tsx (6 tests) 43ms - ✓ src/__tests__/ConnectWidget-test.tsx (1 test) 664ms - ✓ ConnectWidget > renders the real Connect widget and handles WebSocket messages correctly 664ms - ✓ src/components/__tests__/ConnectLogoHeader-test.tsx (4 tests) 45ms - ✓ src/views/loginError/__tests__/LoginError-test.jsx (2 tests) 70ms - ✓ src/views/verification/__tests__/VerifyError-test.tsx (5 tests) 115ms - ✓ src/utilities/__tests__/Browser-test.js (6 tests) 3ms - ✓ src/redux/selectors/__tests__/Connect-test.js (4 tests) 3ms - ✓ src/views/disclosure/__tests__/PrivacyPolicy-test.tsx (3 tests) 163ms - ✓ src/redux/selectors/__tests__/ClientColorScheme-test.js (3 tests) 4ms - ✓ src/views/oauth/__tests__/OAuthDefault-test.tsx (1 test) 91ms - ✓ src/hooks/__tests__/useNavigationPostMessage-test.tsx (2 tests) 17ms - ✓ src/hooks/__tests__/useAnalyticsEvent-test.tsx (2 tests) 67ms - ✓ src/utilities/__tests__/ActionHelpers-test.js (3 tests) 4ms - ✓ src/views/oauth/__tests__/WaitingForOAuth-test.tsx (4 tests) 7176ms - ✓ WaitingForOAuth view > Button delay for try again > should enable the tryAgain button after 2 seconds and call onOAuthRetry when clicked 2028ms - ✓ WaitingForOAuth view > Button delay for try again > should call onOAuthSuccess if polling an oauth state was successful 2514ms - ✓ WaitingForOAuth view > Button delay for try again > should call onOAuthError if polling an oauth state was unsuccessful 2528ms - ✓ src/utilities/__tests__/FormatUrl-test.js (8 tests) 3ms - ✓ src/utilities/__tests__/memberUtils-test.js (2 tests) 2ms - ✓ src/components/__tests__/RenderConnectStep-test.jsx (1 test) 128ms - ✓ src/components/__tests__/ConnectNavigationHeader-test.jsx (3 tests) 79ms -stderr | src/views/oauth/__tests__/OAuthStep-test.tsx > OauthStep view > Ensure OAuthDefault is rendered > should go back to Oauth Default when Try Again button is clicked on the waitingForOAuth screen -Error: Not implemented: window.open - at module.exports (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/not-implemented.js:9:17) - at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/jsdom/lib/jsdom/browser/Window.js:962:7 - at Object.onSignInClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/oauth/OAuthStep.js:201:36) - at onClick (/Users/logan.rasmussen/dev/github-connect-widget/src/views/oauth/OAuthDefault.js:102:19) - at executeDispatch (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19116:9) - at runWithFiberInDEV (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:874:13) - at processDispatchQueue (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19166:19) - at /Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19767:9 - at batchedUpdates$1 (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:3255:40) - at dispatchEventForPluginEventSystem (/Users/logan.rasmussen/dev/github-connect-widget/node_modules/react-dom/cjs/react-dom-client.development.js:19320:7) undefined - - ✓ src/views/oauth/__tests__/OAuthStep-test.tsx (1 test) 168ms - ✓ src/views/disclosure/__tests__/Interstitial-test.tsx (2 tests) 199ms - ✓ src/components/__tests__/MemberError-test.tsx (3 tests) 34ms - ✓ src/redux/reducers/__tests__/experimentalFeaturesSlice-test.ts (3 tests) 3ms - ✓ src/utilities/__tests__/Intl-test.tsx (4 tests) 27ms - ✓ src/components/__tests__/InstitutionGridTile-test.jsx (2 tests) 32ms - ✓ src/components/support/__tests__/GeneralSupport-test.tsx (2 tests) 1444ms - ✓ GeneralSupport > renders generalSupport ticket 1091ms - ✓ GeneralSupport > renders generalSupport ticket and cancels 352ms - ✓ src/utilities/__tests__/timeoutHelper-test.js (5 tests) 2ms - ✓ src/views/mfa/__tests__/MFAStep-test.tsx (2 tests) 190ms - ✓ src/components/support/__tests__/Support-test.tsx (2 tests) 148ms - ✓ src/views/consent/__tests__/DynamicDisclosure-test.tsx (2 tests) 334ms - ✓ src/redux/actions/__tests__/browser-test.js (2 tests) 2ms - ✓ src/redux/__tests__/Store-test.js (5 tests) 2ms - ✓ src/hooks/__tests__/useSelectInstitution-test.tsx (1 test) 112ms - ✓ src/components/__tests__/DayOfMonthPicker-test.tsx (2 tests) 118ms - ✓ src/redux/reducers/__tests__/userFeaturesSlice-test.js (3 tests) 3ms - ✓ src/views/mfa/__tests__/utils-test.js (5 tests) 1ms - ✓ src/utilities/__tests__/Accessibility-test.js (3 tests) 3ms - ✓ src/redux/reducers/__tests__/app-test.js (2 tests) 2ms - ✓ src/redux/actions/__tests__/app-test.js (1 test) 2ms - ✓ src/components/support/__tests__/SupportMenu-test.tsx (2 tests) 71ms - ✓ src/components/__tests__/ClientLogo-test.tsx (2 tests) 18ms - ✓ src/components/__tests__/GoBackButton-test.tsx (3 tests) 59ms - ✓ src/views/manualAccount/__tests__/manualAccountMenu-test.tsx (1 test) 90ms - ✓ src/components/__tests__/AriaLive-test.tsx (2 tests) 18ms - ✓ src/views/disclosure/__tests__/DataAvailable-test.tsx (1 test) 25ms - ✓ src/components/support/__tests__/SupportSuccess-test.tsx (1 test) 435ms - ✓ SupportSuccess > renders and closes when Continue is clicked 434ms - ✓ src/hooks/__tests__/usePollMember-test.tsx (20 tests) 45305ms - ✓ usePollMember > should poll successfully and emit polling states with member and job data 3007ms - ✓ usePollMember > should handle API context not being available 3019ms - ✓ usePollMember > should handle loadMemberByGuid errors gracefully 3025ms - ✓ usePollMember > should handle loadJob errors gracefully 3033ms - ✓ usePollMember > should set initialDataReady when async_account_data_ready is true 3022ms - ✓ usePollMember > should not set initialDataReady when optOutOfEarlyUserRelease is true 3020ms - ✓ usePollMember > should use custom polling interval when provided 513ms - ✓ usePollMember > should increment pollingCount on each poll 2050ms - ✓ usePollMember > should show CHALLENGED status message when member is challenged 3022ms - ✓ usePollMember > should continue polling when member is still being aggregated 3024ms - ✓ usePollMember > should use client locale from html lang attribute 3021ms - ✓ usePollMember > should only set initialDataReady once, even on subsequent polls 2408ms - ✓ usePollMember > should not set initialDataReady when async_account_data_ready is false 3009ms - ✓ usePollMember > should not set initialDataReady when there is an error 3012ms - ✓ usePollMember > should set initialDataReady when async_account_data_ready becomes true after being false 2040ms - ✓ usePollMember > should correctly update previousResponse and currentResponse over multiple polls 2043ms - ✓ usePollMember > should preserve previousResponse and currentResponse when an intermediate poll fails 3012ms - - Test Files 81 passed (81) - Tests 514 passed (514) - Start at 13:57:38 - Duration 46.56s (transform 2.24s, setup 10.30s, collect 248.50s, tests 71.77s, environment 32.83s, prepare 4.75s) - - -> @mxenabled/connect-widget@0.0.0-semantic-release lint -> eslint . --ext ts,tsx,js,jsx,md --report-unused-disable-directives --max-warnings 14 - diff --git a/tmp/planning/CTT-178/plan.md b/tmp/planning/CTT-178/plan.md deleted file mode 100644 index 938a16e8b1..0000000000 --- a/tmp/planning/CTT-178/plan.md +++ /dev/null @@ -1,180 +0,0 @@ -# Implementation Plan: CTT-178 - Restore OAuth Member Update Flow and Dynamic GUID Synchronization - -## 1. Analysis Summary - -- **Type:** New Feature / Architectural Initiative (Reintroduction of flow) -- **Complexity:** Medium -- **Impact:** Critical for non-OAuth to OAuth migrations; ensures session integrity when member GUIDs change during OAuth. -- **Confidence Level:** High - The proposed solution leverages existing Redux actions and RxJS patterns within the widget. - -## 2. Affected Code Areas - -### Primary Files - -- `src/views/oauth/OAuthStep.js` - Re-enable update flow and handle fetched member data. -- `src/views/oauth/WaitingForOAuth.js` - Implement dynamic member GUID detection and fetching. - -### New Files - -- None. - -## 3. Solution Architecture - -```mermaid -sequenceDiagram - participant User - participant OAuthStep - participant API - participant WaitingForOAuth - participant Redux - participant Connecting - - User->>OAuthStep: Select Member for Update - OAuthStep->>API: getOAuthWindowURI(member.guid) - API-->>OAuthStep: oauth_window_uri - OAuthStep->>User: Redirect/Open OAuth Window - User->>WaitingForOAuth: Returns from OAuth - WaitingForOAuth->>API: loadOAuthStates(outbound_member_guid) - API-->>WaitingForOAuth: { inbound_member_guid: 'NEW-MBR' } - Note over WaitingForOAuth: Detected GUID change! - WaitingForOAuth->>API: loadMemberByGuid('NEW-MBR') - API-->>WaitingForOAuth: { member: { guid: 'NEW-MBR', ... } } - WaitingForOAuth->>OAuthStep: onOAuthSuccess('NEW-MBR', member) - OAuthStep->>Redux: dispatch(updateMemberSuccess(member)) - Redux-->>Connecting: state.currentMemberGuid = 'NEW-MBR' - Connecting->>API: pollMember('NEW-MBR') -``` - -## 4. Implementation Plan - -### Phase 1: Restore OAuth Update Flow - -1. **Modify `src/views/oauth/OAuthStep.js`**: Update the `useEffect` that starts the OAuth flow to check for an existing `member` from Redux state if no `pendingOauthMember` is found. -2. **Re-enable `getOAuthWindowURI`**: Ensure that if `member.guid` exists, the flow proceeds to fetch the OAuth URI instead of defaulting to `api.addMember`. - [Validation Checkpoint: Start Connect with `current_member_guid` for an OAuth-compatible institution. Verify it lands on `OAuthStep` and successfully generates the URI for that member.] - -### Phase 2: Dynamic Member Synchronization - -1. **Update `src/views/oauth/WaitingForOAuth.js`**: - - Modify the `oauthStateCompleted$` stream. - - After polling succeeds, check if `inbound_member_guid` differs from the initial `member.guid`. - - If different, use `api.loadMemberByGuid` to fetch the updated member record. - - Update the response object to include the fetched `member`. -2. **Error Handling**: If `loadMemberByGuid` fails, return an error state to trigger the OAuth error view. - [Validation Checkpoint: Mock `loadOAuthStates` to return a different `inbound_member_guid`. Verify that `loadMemberByGuid` is called with the new GUID.] - -### Phase 3: State Reconciliation - -1. **Update `OAuthStep.js` callbacks**: - - Update `handleOAuthSuccess` to accept `(memberGuid, member = null)`. - - If `member` is provided, dispatch `connectActions.updateMemberSuccess({ item: member })`. - - This ensures the Redux store has the new member and `currentMemberGuid` is updated before moving to the `CONNECTING` step. - [Validation Checkpoint: Complete an OAuth flow where the GUID changes. Verify that the `Connecting` view shows the correct institution name and begins polling for the NEW member GUID.] - -## 5. Proposed Code Changes (The Spike) - -### `src/views/oauth/OAuthStep.js` - -```diff -<<<< - if (pendingOauthMember) { - // If there is a pending oauth member, don't create a new one, use that one - member$ = of(pendingOauthMember) - } else { -==== - if (member && member.guid) { - // Use the currently active member (supports Update flows and re-using a member created in this session) - member$ = of(member) - } else if (pendingOauthMember) { - // If no active member, look for a pending oauth member for this institution - member$ = of(pendingOauthMember) - } else { ->>>> -``` - -```diff -<<<< - function handleOAuthSuccess(memberGuid) { - closeOAuthWindow() - dispatch(connectActions.handleOAuthSuccess(memberGuid)) - } -==== - function handleOAuthSuccess(memberGuid, member = null) { - closeOAuthWindow() - if (member) { - dispatch(connectActions.updateMemberSuccess(member)) - } - dispatch(connectActions.handleOAuthSuccess(memberGuid)) - } ->>>> -``` - -### `src/views/oauth/WaitingForOAuth.js` - -```diff -<<<< - map((pollingState) => { - const oauthState = pollingState.currentResponse - - return { - error: oauthState.auth_status === OauthState.AuthStatus.ERRORED, - errorReason: OauthState.ReadableErrorReason[oauthState.error_reason], - memberGuid: oauthState.inbound_member_guid, - } - }), -==== - mergeMap((pollingState) => { - const oauthState = pollingState.currentResponse - const inboundMemberGuid = oauthState.inbound_member_guid - - if (oauthState.auth_status === OauthState.AuthStatus.ERRORED) { - return of({ - error: true, - errorReason: OauthState.ReadableErrorReason[oauthState.error_reason], - memberGuid: inboundMemberGuid || member.guid, - }) - } - - if (inboundMemberGuid && inboundMemberGuid !== member.guid) { - return defer(() => api.loadMemberByGuid(inboundMemberGuid, clientLocale)).pipe( - map((resp) => ({ - error: false, - memberGuid: inboundMemberGuid, - member: resp.member, - })), - catchError(() => - of({ - error: true, - errorReason: 'Failed to fetch updated member information', - memberGuid: inboundMemberGuid, - }), - ), - ) - } - - return of({ - error: false, - memberGuid: inboundMemberGuid || member.guid, - }) - }), ->>>> -``` - -## 6. Validation & Testing - -- [ ] **Unit Tests:** - - `OAuthStep-test.js`: Verify update flow re-activation. - - `WaitingForOAuth-test.js`: Verify dynamic GUID detection and member fetching. -- [ ] **Integration Tests:** Use MSW to simulate a migration scenario where GUID changes from `MBR-OLD` to `MBR-NEW`. -- [ ] **Acceptance Criteria:** - - OAuth update flow is restored. - - State is synchronized with `incoming_member_id`. - - Redux store correctly integrates new members. - -## 7. Risk Mitigation - -- **Risk:** Mangled credentials if updating an existing OAuth member. -> **Mitigation:** The re-enabled flow is specifically for migrations and updates where the backend (Firefly DC-3040) is now handling the credential/member logic safely. -- **Risk:** Infinite polling if new member GUID is invalid. -> **Mitigation:** The fetch step for the new member ensures it exists before proceeding to `Connecting`. -- **Rollback Plan:** Revert changes to `OAuthStep.js` to restore the "always create new member" behavior. - -**Next Step**: Run `/planning:breakdown-plan CTT-178` to break this plan down into actionable tasks. diff --git a/tmp/planning/CTT-178/tasks.md b/tmp/planning/CTT-178/tasks.md deleted file mode 100644 index 6f91c99969..0000000000 --- a/tmp/planning/CTT-178/tasks.md +++ /dev/null @@ -1,53 +0,0 @@ -## Objective & Context - -Restore the OAuth member update flow and implement dynamic GUID synchronization in the Connect Widget. This ensures that users migrating from non-OAuth to OAuth connections can update their existing members correctly, even if the backend returns a different member GUID during the process (e.g., in migration or separation scenarios). - -## Tasks - -- [ ] 0.0 Setup Environment - - [x] 0.1 Verify or create development branch `feature/CTT-178` -- [x] 1.0 Establish Quality Baseline - - [x] 1.1 Run tests and linters to capture baseline metrics and save reports to `tmp/planning/CTT-178/baseline.txt` - -### Implementation Tasks - -- [ ] 2.0 Restore OAuth Update Initiation Flow (Agent: `General Senior Software Agent`) - - [x] 2.1 RED: Update `src/views/oauth/__tests__/OAuthStep-test.tsx` to verify that if an existing member GUID is present, it is prioritized for OAuth URI generation. - - [x] 2.2 GREEN: Modify `src/views/oauth/OAuthStep.js` to check for `member.guid` before `pendingOauthMember` in the OAuth initialization `useEffect`. - - [x] 2.3 REFACTOR: Ensure the conditional logic for URI generation is clear and adheres to project standards. - - [x] 2.4 CLEANUP: Run linter and verify `OAuthStep-test.tsx` passes. - - [x] 2.5 Create git checkpoint: `feat(oauth): prioritize existing member for update flow` - -- [ ] 3.0 Implement Dynamic GUID Synchronization Logic (Agent: `General Senior Software Agent`) - - [x] 3.1 RED: Update `src/views/oauth/__tests__/WaitingForOAuth-test.tsx` to simulate a scenario where `inbound_member_guid` differs from the current member GUID and verify `api.loadMemberByGuid` is called. - - [x] 3.2 GREEN: Modify the `oauthStateCompleted$` stream in `src/views/oauth/WaitingForOAuth.js` to detect GUID changes and fetch the new member record using `api.loadMemberByGuid`. - - [x] 3.3 REFACTOR: Optimize the RxJS pipeline (using `mergeMap` and `defer`) and ensure robust error handling for the member fetch. - - [x] 3.4 CLEANUP: Run linter and verify `WaitingForOAuth-test.tsx` passes. - - [x] 3.5 Create git checkpoint: `feat(oauth): implement dynamic member guid synchronization` - -- [ ] 4.0 Enhance Redux State Reconciliation (Agent: `General Senior Software Agent`) - - [x] 4.1 RED: Add a test case to `src/views/oauth/__tests__/OAuthStep-test.tsx` verifying that `handleOAuthSuccess` dispatches `updateMemberSuccess` when a member object is provided. - - [x] 4.2 GREEN: Update `handleOAuthSuccess` in `src/views/oauth/OAuthStep.js` to accept an optional `member` parameter and dispatch `connectActions.updateMemberSuccess` if present. - - [x] 4.3 REFACTOR: Ensure the callback signature and dispatch logic are clean and type-safe. - - [x] 4.4 CLEANUP: Run linter and verify all OAuth tests pass. - - [x] 4.5 Create git checkpoint: `feat(oauth): dispatch member update on successful sync` - -- [ ] 5.0 Verify Migration and Synchronization Scenarios (Agent: `General Senior Software Agent`) - - [x] 5.1 RED: Create or update an integration test (e.g., in `src/__tests__/ConnectWidget-test.tsx`) using `apiValue` mocking to simulate a full migration flow where the GUID changes from `MBR-OLD` to `MBR-NEW`. - - [x] 5.2 GREEN: Ensure the widget correctly transitions from `WaitingForOAuth` to `Connecting` with the new GUID and correct institution data. - - [x] 5.3 REFACTOR: Clean up mock data and test utilities used for the integration test. - - [x] 5.4 CLEANUP: Run full test suite and verify all integration tests pass. - - [x] 5.5 Create git checkpoint: `test(oauth): add integration test for guid migration scenario` - -- [x] 6.0 Verification & Regression Check (Agent: `General Senior Software Agent`) - - [x] 6.1 Run full test suite and verify no regressions were introduced to existing OAuth or non-OAuth flows. - - [x] 6.2 Run linters to verify code quality has not decreased compared to baseline. - -- [x] 7.0 Review and Update Documentation (Agent: `tech-lead`) - - [x] 7.1 DOCS: Update `docs/APIDOCUMENTATION.md` and `docs/USER_FEATURES.md` to reflect the reintroduced OAuth update flow and the dynamic GUID synchronization logic. - - [x] 7.2 Create git checkpoint: `docs(oauth): document update flow and guid synchronization` - -- [x] 8.0 Finalize Task - - [x] 8.1 Squash all checkpoint commits into a single clean conventional commit: `feat(oauth): restore update flow and implement dynamic guid sync` - -**Next Step**: Run `/swe:implement-plan CTT-178` to begin implementation. From b7095dcdfc63ac1768dc28678e5e7e54c8c15838 Mon Sep 17 00:00:00 2001 From: Logan Rasmussen Date: Thu, 30 Apr 2026 15:46:09 -0600 Subject: [PATCH 4/5] fix(git): ignore tmp folder --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6e4f3ce9a5..aea1036b67 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ dist-ssr *.sln *.sw? .tool-versions + +tmp \ No newline at end of file From 614f063c67269c159f99cf99520e7840b28b93de Mon Sep 17 00:00:00 2001 From: Wes Risenmay Date: Fri, 1 May 2026 15:11:01 -0600 Subject: [PATCH 5/5] add an abstraction so we don't have to pass the store into the connect widget --- src/ConnectWidget.tsx | 37 ++++++++++++++++------------ src/__tests__/ConnectWidget-test.tsx | 8 +++--- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/ConnectWidget.tsx b/src/ConnectWidget.tsx index a9ea09faaa..13a6b193cd 100644 --- a/src/ConnectWidget.tsx +++ b/src/ConnectWidget.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import React, { createContext, useEffect } from 'react' -import { Provider } from 'react-redux' +import { Provider, useDispatch } from 'react-redux' import Store from 'src/redux/Store' import Connect from 'src/Connect' @@ -19,35 +19,40 @@ interface PostMessageContextType { export const PostMessageContext = createContext({ onPostMessage: () => {} }) -export const ConnectWidget = ({ +export const ConnectWidgetWithoutReduxProvider = ({ onPostMessage = () => {}, onAnalyticPageview = () => {}, postMessageEventOverrides, showTooSmallDialog = true, webSocketConnection, - store = Store, ...props }: any) => { initGettextLocaleData(props.language) + const dispatch = useDispatch() + useEffect(() => { - store.dispatch(setLocalizedContent(props?.language?.localizedContent || {})) + dispatch(setLocalizedContent(props?.language?.localizedContent || {})) }, []) return ( - - - - - - {showTooSmallDialog && } - - - - - - + + + + + {showTooSmallDialog && } + + + + + ) } +export const ConnectWidget = (props: any) => ( + + + +) + export default ConnectWidget diff --git a/src/__tests__/ConnectWidget-test.tsx b/src/__tests__/ConnectWidget-test.tsx index 6033e76a7c..9c36b31e2f 100644 --- a/src/__tests__/ConnectWidget-test.tsx +++ b/src/__tests__/ConnectWidget-test.tsx @@ -4,7 +4,7 @@ import { Subject } from 'rxjs' import { act } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { ConnectWidget } from '../ConnectWidget' +import { ConnectWidgetWithoutReduxProvider } from '../ConnectWidget' import { render, screen, waitFor, createTestReduxStore } from 'src/utilities/testingLibrary' import { apiValue as apiValueMock } from 'src/const/apiProviderMock' import { member, JOB_DATA, OAUTH_STATE, initialState, masterData } from 'src/services/mockedData' @@ -54,12 +54,11 @@ describe('ConnectWidget', () => { const clientConfig = { mode: 'aggregation', current_member_guid: 'MBR-123' } render( - , { apiValue: mockApiValue, store: activeStore }, @@ -137,11 +136,10 @@ describe('ConnectWidget', () => { activeStore = createTestReduxStore(preloadedState) render( - , { apiValue: mockApiValue,