Skip to content

Commit 64e8060

Browse files
authored
Add referrer redirect support to OIDC auth completion (v1.3.0) (#15)
* Bump version to 1.2.0 (minor) Made-with: Cursor * Add referrer redirect support to OIDC auth completion (v1.3.0) After OIDC authentication completes, check for a stored referrer redirect. If present, navigate to the stored URL (with optional #angie-prompt= hash) instead of opening the sidebar. This enables prompt-after-redirect flows where a user is sent through auth and then redirected back with a prompt. Made-with: Cursor
1 parent d8a9c17 commit 64e8060

File tree

3 files changed

+167
-5
lines changed

3 files changed

+167
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@elementor/angie-sdk",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "TypeScript SDK for Angie AI assistant",
55
"main": "dist/index.cjs",
66
"module": "dist/index.js",

src/oauth.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals';
2+
3+
const mockSetupOidcAuthParentListener = jest.fn();
4+
const mockForwardOidcLoginFlowToWindow = jest.fn();
5+
6+
jest.mock( '@elementor/oidc-auth', () => ( {
7+
setupOidcAuthParentListener: mockSetupOidcAuthParentListener,
8+
forwardOidcLoginFlowToWindow: mockForwardOidcLoginFlowToWindow,
9+
} ) );
10+
11+
const mockGetReferrerRedirect = jest.fn();
12+
const mockClearReferrerRedirect = jest.fn();
13+
14+
jest.mock( './referrer-redirect', () => ( {
15+
getReferrerRedirect: mockGetReferrerRedirect,
16+
clearReferrerRedirect: mockClearReferrerRedirect,
17+
} ) );
18+
19+
jest.mock( './config', () => ( {
20+
appState: {
21+
iframeUrlObject: { origin: 'https://angie.test.com' },
22+
iframe: document.createElement( 'iframe' ),
23+
},
24+
} ) );
25+
26+
jest.mock( './logger', () => ( {
27+
createChildLogger: () => ( {
28+
log: jest.fn(),
29+
warn: jest.fn(),
30+
error: jest.fn(),
31+
} ),
32+
} ) );
33+
34+
import { listenToOAuthFromIframe, setupOidcLoginFlowHandler } from './oauth';
35+
36+
describe( 'oauth', () => {
37+
beforeEach( () => {
38+
jest.clearAllMocks();
39+
mockGetReferrerRedirect.mockReturnValue( null );
40+
41+
Object.defineProperty( window, 'toggleAngieSidebar', {
42+
value: jest.fn(),
43+
writable: true,
44+
configurable: true,
45+
} );
46+
} );
47+
48+
afterEach( () => {
49+
jest.restoreAllMocks();
50+
} );
51+
52+
function getOidcParentCallback(): () => void {
53+
listenToOAuthFromIframe();
54+
return ( mockSetupOidcAuthParentListener.mock.calls[ 0 ][ 0 ] as any ).onOAuthParamsCleared;
55+
}
56+
57+
function getOidcLoginFlowCallback(): () => void {
58+
setupOidcLoginFlowHandler();
59+
const lastIdx = mockForwardOidcLoginFlowToWindow.mock.calls.length - 1;
60+
return ( mockForwardOidcLoginFlowToWindow.mock.calls[ lastIdx ][ 0 ] as any ).onSuccess;
61+
}
62+
63+
describe( 'listenToOAuthFromIframe', () => {
64+
it( 'should setup OIDC auth parent listener', () => {
65+
// Act
66+
listenToOAuthFromIframe();
67+
68+
// Assert
69+
expect( mockSetupOidcAuthParentListener ).toHaveBeenCalledWith( {
70+
trustedOrigin: 'https://angie.test.com',
71+
onOAuthParamsCleared: expect.any( Function ),
72+
} );
73+
} );
74+
75+
it( 'should open sidebar when auth completes and no referrer redirect', () => {
76+
// Arrange
77+
jest.useFakeTimers();
78+
const callback = getOidcParentCallback();
79+
80+
// Act
81+
callback();
82+
jest.advanceTimersByTime( 500 );
83+
84+
// Assert
85+
expect( window.toggleAngieSidebar ).toHaveBeenCalledWith( true );
86+
87+
jest.useRealTimers();
88+
} );
89+
90+
it( 'should redirect with prompt when referrer redirect with prompt exists', () => {
91+
// Arrange
92+
const returnUrl = 'http://localhost/wp-admin/post.php?post=123';
93+
const prompt = 'Help me create a contact page';
94+
mockGetReferrerRedirect.mockReturnValue( { url: returnUrl, prompt } );
95+
const callback = getOidcParentCallback();
96+
97+
// Act
98+
callback();
99+
100+
// Assert
101+
expect( mockClearReferrerRedirect ).toHaveBeenCalled();
102+
expect( window.toggleAngieSidebar ).not.toHaveBeenCalled();
103+
} );
104+
105+
it( 'should redirect without prompt hash when referrer redirect exists without prompt', () => {
106+
// Arrange
107+
const returnUrl = 'http://localhost/wp-admin/post.php?post=123';
108+
mockGetReferrerRedirect.mockReturnValue( { url: returnUrl } );
109+
const callback = getOidcParentCallback();
110+
111+
// Act
112+
callback();
113+
114+
// Assert
115+
expect( mockClearReferrerRedirect ).toHaveBeenCalled();
116+
expect( window.toggleAngieSidebar ).not.toHaveBeenCalled();
117+
} );
118+
} );
119+
120+
describe( 'setupOidcLoginFlowHandler', () => {
121+
it( 'should forward OIDC login flow with redirect callback', () => {
122+
// Act
123+
setupOidcLoginFlowHandler();
124+
125+
// Assert
126+
expect( mockForwardOidcLoginFlowToWindow ).toHaveBeenCalledWith( {
127+
targets: expect.any( Object ),
128+
onSuccess: expect.any( Function ),
129+
} );
130+
} );
131+
132+
it( 'should redirect when OIDC login succeeds and referrer redirect exists', () => {
133+
// Arrange
134+
const returnUrl = 'http://localhost/wp-admin/post.php?post=456';
135+
const prompt = 'Help me optimize SEO';
136+
mockGetReferrerRedirect.mockReturnValue( { url: returnUrl, prompt } );
137+
const callback = getOidcLoginFlowCallback();
138+
139+
// Act
140+
callback();
141+
142+
// Assert
143+
expect( mockClearReferrerRedirect ).toHaveBeenCalled();
144+
} );
145+
} );
146+
} );

src/oauth.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from "@elementor/oidc-auth";
66
import { appState } from "./config";
77
import { createChildLogger } from "./logger";
8+
import { clearReferrerRedirect, getReferrerRedirect } from "./referrer-redirect";
89

910
declare global {
1011
interface Window {
@@ -14,7 +15,22 @@ declare global {
1415

1516
const logger = createChildLogger( 'oauth' );
1617

17-
function openSidebarAfterAuthentication(): void {
18+
function buildRedirectUrl( url: string, prompt?: string ): string {
19+
if ( ! prompt ) {
20+
return url;
21+
}
22+
return `${ url }#angie-prompt=${ encodeURIComponent( prompt ) }`;
23+
}
24+
25+
function onAuthenticationComplete(): void {
26+
const redirectData = getReferrerRedirect();
27+
28+
if ( redirectData ) {
29+
clearReferrerRedirect();
30+
window.location.href = buildRedirectUrl( redirectData.url, redirectData.prompt );
31+
return;
32+
}
33+
1834
try {
1935
localStorage.setItem( 'angie_sidebar_state', 'open' );
2036
} catch ( e ) {
@@ -28,7 +44,7 @@ function openSidebarAfterAuthentication(): void {
2844
export const listenToOAuthFromIframe = (): void => {
2945
setupOidcAuthParentListener( {
3046
trustedOrigin: appState.iframeUrlObject?.origin ?? '',
31-
onOAuthParamsCleared: openSidebarAfterAuthentication,
47+
onOAuthParamsCleared: onAuthenticationComplete,
3248
} );
3349
};
3450

@@ -37,8 +53,8 @@ export const setupOidcLoginFlowHandler = (): void => {
3753

3854
window.addEventListener( 'load', () => {
3955
logger.log( 'OIDC: Window load event fired, forwarding OIDC state if present' );
40-
forwardOidcLoginFlowToWindow( { targets, onSuccess: openSidebarAfterAuthentication } );
56+
forwardOidcLoginFlowToWindow( { targets, onSuccess: onAuthenticationComplete } );
4157
} );
4258

43-
forwardOidcLoginFlowToWindow( { targets, onSuccess: openSidebarAfterAuthentication } );
59+
forwardOidcLoginFlowToWindow( { targets, onSuccess: onAuthenticationComplete } );
4460
};

0 commit comments

Comments
 (0)