Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.log
.DS_Store
node_modules
.cache
dist
dist-next
coverage
*.tgz
*.test*
11 changes: 10 additions & 1 deletion src/context/HubScriptInjector/HubScriptInjector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

declare global {
interface Window {
_rphConfig: any;

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 6 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Unexpected any. Specify a different type
}
}

function setConfigValue(key: string, value: any) {

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 10 in src/context/HubScriptInjector/HubScriptInjector.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Unexpected any. Specify a different type
if (!value) {
return;
}
Expand All @@ -15,11 +15,18 @@
window?._rphConfig.push([key, value]);
}

/**
* Default API version for new SDK releases.
* This controls which Hub features are enabled by default.
*/
const DEFAULT_API_VERSION = '2026-01-21';

export type HubScriptInjectorProps = {
appKey: string;
stateListener: ({ state, api }: HubListenerProps) => void;
hubUrlOverride?: string;
locationHash?: string;
apiVersion?: string;
};

// Grab the URL hash ASAP in case it contains an `rph_init` param
Expand All @@ -30,6 +37,7 @@
appKey,
hubUrlOverride,
stateListener,
apiVersion = DEFAULT_API_VERSION,
...rest
}: HubScriptInjectorProps) {

Expand Down Expand Up @@ -66,6 +74,7 @@
setConfigValue('setAppKey', appKey);
setConfigValue('setStateListener', stateListener);
setConfigValue('setLocationHash', locationHash);
setConfigValue('setApiVersion', apiVersion);

if (window.localStorage.getItem('rph_log_level') === 'debug') {
console.debug('[debug] rest:', rest);
Expand All @@ -83,7 +92,7 @@
console.debug('[debug] hubConfig:', window._rphConfig);
}
}
}, [appKey, stateListener, locationHash, hubUrlOverride, rest]);
}, [appKey, stateListener, locationHash, hubUrlOverride, apiVersion, rest]);

return null;
}
6 changes: 6 additions & 0 deletions src/context/RowndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
export const RowndContext = createContext<TRowndContext | undefined>(undefined);

export type HubListenerProps = {
state: any;

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 7 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Unexpected any. Specify a different type
api: any;

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and ubuntu-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and macOS-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 22.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 20.x and windows-latest

Unexpected any. Specify a different type

Check warning on line 8 in src/context/RowndContext.tsx

View workflow job for this annotation

GitHub Actions / Build, lint, and test on Node 18.x and windows-latest

Unexpected any. Specify a different type
};

export type RowndProviderProps = {
Expand All @@ -15,6 +15,12 @@
hubUrlOverride?: string;
postRegistrationUrl?: string;
postSignOutRedirect?: string;
/**
* API version date string (e.g., '2026-01-21') that controls which Hub features are enabled.
* Defaults to the current SDK version date for new features.
* Set to an earlier date to opt-out of newer behaviors.
*/
apiVersion?: string;
children: React.ReactNode;
};

Expand Down
155 changes: 123 additions & 32 deletions src/next/client/components/RowndServerStateSync.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ vi.mock('../../../ssr/hooks/useCookie');
describe('RowndServerStateSync', () => {
const mockCookieSignIn = vi.fn();
const mockCookieSignOut = vi.fn();
let mockReload: ReturnType<typeof vi.fn>;

beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();

// Mock window.location.reload
const mockReload = vi.fn();
mockReload = vi.fn();
Object.defineProperty(window, 'location', {
value: { reload: mockReload },
writable: true
Expand All @@ -43,53 +44,143 @@ describe('RowndServerStateSync', () => {
expect(mockCookieSignOut).not.toHaveBeenCalled();
});

it('should trigger cookieSignIn when access token becomes available', () => {
(useRownd as any).mockReturnValue({
access_token: 'new-token',
is_initializing: false
describe('initial load (first render after initialization)', () => {
it('should sync cookie without reload when token exists on initial load', () => {
(useRownd as any).mockReturnValue({
access_token: 'existing-token',
is_initializing: false
});

render(<RowndServerStateSync />);

// Should sync cookie
expect(mockCookieSignIn).toHaveBeenCalledTimes(1);
// Should NOT pass a reload callback - just sync silently
expect(mockCookieSignIn).toHaveBeenCalledWith();
expect(mockCookieSignOut).not.toHaveBeenCalled();
});

render(<RowndServerStateSync />);
it('should not trigger any actions when no token on initial load', () => {
(useRownd as any).mockReturnValue({
access_token: null,
is_initializing: false
});

expect(mockCookieSignIn).toHaveBeenCalled();
expect(mockCookieSignOut).not.toHaveBeenCalled();
});
render(<RowndServerStateSync />);

it('should trigger cookieSignOut when access token is removed', () => {
(useRownd as any).mockReturnValue({
access_token: null,
is_initializing: false
// No token on initial load means user wasn't signed in - nothing to sync
expect(mockCookieSignIn).not.toHaveBeenCalled();
expect(mockCookieSignOut).not.toHaveBeenCalled();
});
});

render(<RowndServerStateSync />);
describe('sign-in (null -> token)', () => {
it('should trigger cookieSignIn with reload callback when signing in', () => {
const mockUseRownd = useRownd as any;

expect(mockCookieSignOut).toHaveBeenCalled();
expect(mockCookieSignIn).not.toHaveBeenCalled();
// Initial render with no token
mockUseRownd.mockReturnValue({
access_token: null,
is_initializing: false
});

const { rerender } = render(<RowndServerStateSync />);
vi.clearAllMocks();

// User signs in - token becomes available
mockUseRownd.mockReturnValue({
access_token: 'new-token',
is_initializing: false
});

rerender(<RowndServerStateSync />);

expect(mockCookieSignIn).toHaveBeenCalledTimes(1);
// Should pass a reload callback for sign-in
expect(mockCookieSignIn).toHaveBeenCalledWith(expect.any(Function));
expect(mockCookieSignOut).not.toHaveBeenCalled();
});
});

it('should not trigger any cookie actions when access token remains the same', () => {
const mockUseRownd = useRownd as any;
describe('token refresh (token -> different token)', () => {
it('should sync cookie silently without reload on token refresh', () => {
const mockUseRownd = useRownd as any;

// Initial render with a token
mockUseRownd.mockReturnValue({
access_token: 'original-token',
is_initializing: false
});

// Initial render with a token
mockUseRownd.mockReturnValue({
access_token: 'same-token',
is_initializing: false
const { rerender } = render(<RowndServerStateSync />);
vi.clearAllMocks();

// Token refreshes - different token
mockUseRownd.mockReturnValue({
access_token: 'refreshed-token',
is_initializing: false
});

rerender(<RowndServerStateSync />);

// Should sync cookie silently (no reload callback)
expect(mockCookieSignIn).toHaveBeenCalledTimes(1);
expect(mockCookieSignIn).toHaveBeenCalledWith();
expect(mockCookieSignOut).not.toHaveBeenCalled();
});
});

const { rerender } = render(<RowndServerStateSync />);
describe('sign-out (token -> null)', () => {
it('should trigger cookieSignOut with reload callback when signing out', () => {
const mockUseRownd = useRownd as any;

// Clear the mock calls from initial render
vi.clearAllMocks();
// Initial render with a token
mockUseRownd.mockReturnValue({
access_token: 'existing-token',
is_initializing: false
});

const { rerender } = render(<RowndServerStateSync />);
vi.clearAllMocks();

// User signs out - token removed
mockUseRownd.mockReturnValue({
access_token: null,
is_initializing: false
});

rerender(<RowndServerStateSync />);

// Rerender with the same token
mockUseRownd.mockReturnValue({
access_token: 'same-token',
is_initializing: false
expect(mockCookieSignOut).toHaveBeenCalledTimes(1);
// Should pass a reload callback for sign-out
expect(mockCookieSignOut).toHaveBeenCalledWith(expect.any(Function));
expect(mockCookieSignIn).not.toHaveBeenCalled();
});
});

rerender(<RowndServerStateSync />);
describe('no change (same token)', () => {
it('should not trigger any cookie actions when access token remains the same', () => {
const mockUseRownd = useRownd as any;

expect(mockCookieSignIn).not.toHaveBeenCalled();
expect(mockCookieSignOut).not.toHaveBeenCalled();
// Initial render with a token
mockUseRownd.mockReturnValue({
access_token: 'same-token',
is_initializing: false
});

const { rerender } = render(<RowndServerStateSync />);
vi.clearAllMocks();

// Rerender with the same token
mockUseRownd.mockReturnValue({
access_token: 'same-token',
is_initializing: false
});

rerender(<RowndServerStateSync />);

expect(mockCookieSignIn).not.toHaveBeenCalled();
expect(mockCookieSignOut).not.toHaveBeenCalled();
});
});
});
32 changes: 29 additions & 3 deletions src/next/client/components/RowndServerStateSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,51 @@ const RowndServerStateSync = () => {

// Trigger cookieSignIn when new accessToken is available.
const prevAccessToken = useRef<string | null | undefined>(undefined);
const hasInitialized = useRef(false);

useEffect(() => {
if (is_initializing) {
return;
}

// Track initialization state
if (!hasInitialized.current) {
hasInitialized.current = true;
prevAccessToken.current = access_token;

// If we have a token on initial load, sync it without reload
if (access_token) {
cookieSignIn();
}
return;
}

if (prevAccessToken.current === access_token) {
return;
}

const wasSignedOut = !prevAccessToken.current;
const isSigningIn = wasSignedOut && !!access_token;
const isSigningOut = !!prevAccessToken.current && !access_token;
const isTokenRefresh = !!prevAccessToken.current && !!access_token;

prevAccessToken.current = access_token;

if (access_token) {
if (isSigningIn) {
// User just signed in - sync cookie and reload
// This ensures server has the cookie for initial authenticated render
cookieSignIn(() => window.location.reload());
return;
}

// Handle sign out
if (!access_token) {
if (isTokenRefresh) {
// Token was refreshed in background - sync cookie silently, no reload
cookieSignIn();
return;
}

if (isSigningOut) {
// User signed out - sync cookie and reload
cookieSignOut(() => window.location.reload());
}
}, [
Expand Down
Loading