Skip to content

Commit 95f42ad

Browse files
committed
Signup: suspend Blackbox on inactive social-first screens
Both screens of the social-first signup stay mounted and are hidden via CSS visibility stacking, so the challenge widget (which sets its own visibility inside a closed shadow root) bled through onto the selection screen and consumed a session before email was even picked. Add a `suspended` flag to `useBlackboxProtection` (exposed as a `blackboxSuspended` prop by `withBlackboxProtection`) and gate each stacked `PasswordlessSignupForm` on its screen being active. `useBlackbox` now sets loading when enabled after mount and clears blocking state when suspended mid-challenge.
1 parent 2e5c10b commit 95f42ad

5 files changed

Lines changed: 80 additions & 7 deletions

File tree

client/blocks/login/use-blackbox-protection.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ interface UseBlackboxProtectionOptions {
2222
* `getSessionId` is a no-op so no SDK load/collect happens.
2323
*/
2424
feature: string;
25+
/**
26+
* Keep Blackbox off while the host form is mounted but not the active
27+
* surface (e.g. hidden behind another step). No collect happens and no
28+
* challenge can render until this flips back to false.
29+
*/
30+
suspended?: boolean;
2531
}
2632

2733
const noopGetSessionId = () => Promise.resolve( undefined );
@@ -31,8 +37,10 @@ const noopGetSessionId = () => Promise.resolve( undefined );
3137
*/
3238
export function useBlackboxProtection( {
3339
feature,
40+
suspended,
3441
}: UseBlackboxProtectionOptions ): BlackboxProtection {
3542
const enabled =
43+
! suspended &&
3644
!! config( 'blackbox_api_key' ) &&
3745
config.isEnabled( 'blackbox' ) &&
3846
config.isEnabled( feature );

client/blocks/login/utils/test/use-blackbox.jsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ jest.mock( '../blackbox-sdk', () => ( {
1111
loadBlackboxSdk: jest.fn( () => Promise.resolve() ),
1212
} ) );
1313

14-
function TestComponent() {
14+
function TestComponent( { enabled = true } ) {
1515
const containerRef = useRef( null );
1616
const { hasChallengeContent, isChallengeActive, isLoading } = useBlackbox( {
1717
containerRef,
18-
enabled: true,
18+
enabled,
1919
} );
2020

2121
return (
@@ -119,4 +119,48 @@ describe( 'useBlackbox', () => {
119119

120120
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'ready/inactive/empty' );
121121
} );
122+
123+
test( 'does not load the SDK while disabled', async () => {
124+
render( <TestComponent enabled={ false } /> );
125+
126+
await act( async () => {} );
127+
128+
expect( loadBlackboxSdk ).not.toHaveBeenCalled();
129+
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'ready/inactive/empty' );
130+
} );
131+
132+
test( 'starts loading and configures when enabled after mount', async () => {
133+
const { rerender } = render( <TestComponent enabled={ false } /> );
134+
135+
await act( async () => {} );
136+
rerender( <TestComponent enabled /> );
137+
138+
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'loading/inactive/empty' );
139+
140+
await act( async () => {} );
141+
142+
expect( window.Blackbox.configure ).toHaveBeenCalled();
143+
144+
act( () => jest.advanceTimersByTime( 500 ) );
145+
146+
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'ready/inactive/empty' );
147+
} );
148+
149+
test( 'clears blocking state when disabled mid-challenge', async () => {
150+
let callbacks;
151+
window.Blackbox.configure.mockImplementationOnce( ( config ) => {
152+
callbacks = config;
153+
} );
154+
155+
const { rerender } = render( <TestComponent /> );
156+
157+
await act( async () => {} );
158+
act( () => callbacks.onChallengeStart() );
159+
160+
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'ready/active/empty' );
161+
162+
rerender( <TestComponent enabled={ false } /> );
163+
164+
expect( screen.getByTestId( 'blackbox-state' ) ).toHaveTextContent( 'ready/inactive/empty' );
165+
} );
122166
} );

client/blocks/login/utils/use-blackbox.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,18 @@ export function useBlackbox( { containerRef, enabled } ) {
4848

4949
useEffect( () => {
5050
if ( ! isEnabled ) {
51+
// Covers the surface being suspended after a challenge appeared: drop
52+
// all blocking state so the form isn't wedged when it re-enables.
53+
setIsLoading( false );
54+
setIsChallengeActive( false );
55+
setHasChallengeContent( false );
5156
return;
5257
}
5358

59+
// The initial state only covers enabled-at-mount; this covers a surface
60+
// that enables later (e.g. a hidden signup step becoming active).
61+
setIsLoading( true );
62+
5463
let cancelled = false;
5564
let hasStartedChallenge = false;
5665
let settleTimeout;

client/blocks/login/with-blackbox-protection.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ interface WithBlackboxProtectionOptions {
77
feature: string;
88
}
99

10+
interface WithBlackboxProtectionProps {
11+
/** Suspend Blackbox while the host form is mounted but not the active surface. */
12+
blackboxSuspended?: boolean;
13+
}
14+
1015
/**
1116
* HOC that injects `useBlackboxProtection()` as a `blackbox` prop, for class
1217
* components that can't call the hook directly.
1318
*/
1419
export function withBlackboxProtection< P extends object >(
1520
WrappedComponent: ComponentType< P & { blackbox: BlackboxProtection } >,
1621
options: WithBlackboxProtectionOptions
17-
): ComponentType< P > {
18-
return function WithBlackboxProtection( props: P ) {
19-
const blackbox = useBlackboxProtection( options );
20-
return <WrappedComponent { ...props } blackbox={ blackbox } />;
22+
): ComponentType< P & WithBlackboxProtectionProps > {
23+
return function WithBlackboxProtection( {
24+
blackboxSuspended,
25+
...props
26+
}: P & WithBlackboxProtectionProps ) {
27+
const blackbox = useBlackboxProtection( { ...options, suspended: blackboxSuspended } );
28+
return <WrappedComponent { ...( props as P ) } blackbox={ blackbox } />;
2129
};
2230
}

client/blocks/signup-form/signup-form-social-first.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ const SignupFormSocialFirst = ( {
213213

214214
const emailLoginBlock = isEmailFirstVariant ? (
215215
<div className="signup-form-social-first-email">
216-
<PasswordlessSignupForm { ...passwordlessFormProps } />
216+
<PasswordlessSignupForm
217+
{ ...passwordlessFormProps }
218+
blackboxSuspended={ currentStep !== 'initial' }
219+
/>
217220
</div>
218221
) : null;
219222

@@ -287,6 +290,7 @@ const SignupFormSocialFirst = ( {
287290
<div className={ getVisibilityClassName( 'email' ) }>
288291
<div className="signup-form-social-first-email">
289292
<PasswordlessSignupForm
293+
blackboxSuspended={ currentStep !== 'email' }
290294
stepName={ stepName }
291295
flowName={ flowName }
292296
goToNextStep={ goToNextStep }

0 commit comments

Comments
 (0)