Skip to content

Commit

Permalink
fix: support react-19 (#860) RELEASE
Browse files Browse the repository at this point in the history
## Related Issues

Fixes descope/etc#8299
## Description

The error occurs in React 19 due to changes in how React handles Custom
Elements and their properties.

before (18) React was more lenient when setting properties on Custom
Elements. If a property had only a getter and no setter, React would
often silently fail without throwing an error.
In React 19 - react has improved its support for Custom Elements and
aligns with the HTML specification. When React attempts to set a
property on a Custom Element, and the property has only a getter (no
setter), it throws a TypeError since it violates JavaScript’s property
descriptor rules

some sources:
https://blog.logrocket.com/working-custom-elements-react
facebook/react#29037

https://codingmall.com/knowledge-base/25-global/19398-how-does-react-19-support-custom-elements-differently-from-previous-versions

another approach will be also to change getters to internal
(`_getTheme`) and avoid setters, might work as well
  • Loading branch information
asafshen authored Dec 22, 2024
1 parent c4182b3 commit efd6833
Show file tree
Hide file tree
Showing 29 changed files with 1,045 additions and 565 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ const createValidateAttributesMixin =
}

async init() {
await super.init?.();

// check attributes initial values
mappingsNames.forEach((attr) =>
this.#handleError(attr, this.getAttribute(attr)),
);
await super.init?.();
}
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const loggerMixin = createSingletonMixin(
}, {}) as Logger;
}

set logger(logger: Partial<Logger>) {
this.#logger = this.#wrapLogger(logger);
set logger(logger: Partial<Logger> | undefined) {
this.#logger = this.#wrapLogger(logger || console);
}

get logger(): Logger {
Expand Down
2 changes: 1 addition & 1 deletion packages/sdks/nextjs-sdk/examples/app-router/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
3 changes: 0 additions & 3 deletions packages/sdks/nextjs-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/jest": "^29.5.12",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.18",
"@types/react-router-dom": "^5.3.3",
"babel": "^6.23.0",
"babel-jest": "^27.5.1",
"eslint": "8.56.0",
Expand Down
9 changes: 8 additions & 1 deletion packages/sdks/nextjs-sdk/src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { NextApiRequest } from 'next';
import { headers } from 'next/headers';
import { DESCOPE_SESSION_HEADER } from './constants';

// This type is declared to allow simpler migration to Next.15
// It will be removed in the future
type HeaderTypes = Awaited<ReturnType<typeof headers>>;

const extractSession = (
descopeSession?: string
): AuthenticationInfo | undefined => {
Expand All @@ -21,7 +25,10 @@ const extractSession = (
// returns the session token if it exists in the headers
// This function require middleware
export const session = (): AuthenticationInfo | undefined => {
const sessionHeader = headers()?.get(DESCOPE_SESSION_HEADER);
// from Next.js 15, headers() returns a Promise
// It can still be used synchronously to facilitate migration
const reqHeaders = headers() as never as HeaderTypes;
const sessionHeader = reqHeaders.get(DESCOPE_SESSION_HEADER);
return extractSession(sessionHeader);
};

Expand Down
4 changes: 2 additions & 2 deletions packages/sdks/react-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ const App = () => {
// - "skipFirstScreen": automatically focus on the first input of each screen, except first screen
// autoFocus="skipFirstScreen"

// validateOnBlur: set it to true will show input validation errors on blur, in addition to on submit
// validateOnBlur: set it to true will show input validation errors on blur, in addition to on submit

// restartOnError: if set to true, in case of flow version mismatch, will restart the flow if the components version was not changed. Default is false

// errorTransformer is a function that receives an error object and returns a string. The returned string will be displayed to the user.
Expand Down
2 changes: 1 addition & 1 deletion packages/sdks/react-sdk/examples/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Layout = () => (
</div>
);

const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { isAuthenticated, isSessionLoading } = useSession();

if (isSessionLoading) {
Expand Down
2 changes: 2 additions & 0 deletions packages/sdks/react-sdk/examples/app/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ const Login = () => {
client={{ version: '1.0.2' }} // found in context key: client.version
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
locale={process.env.DESCOPE_LOCALE as string}
redirectUrl={process.env.DESCOPE_REDIRECT_URL}
tenant={process.env.DESCOPE_TENANT_ID}
telemetryKey={process.env.DESCOPE_TELEMETRY_KEY}
errorTransformer={errorTransformer}
logger={console}
/>
)}
{errorMessage && (
Expand Down
8 changes: 8 additions & 0 deletions packages/sdks/react-sdk/examples/app/ManageAccessKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,19 @@ const ManageAccessKeys = () => {
<AccessKeyManagement
widgetId="access-key-management-widget"
tenant={process.env.DESCOPE_TENANT_ID!}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
<h2>Manage My Access Keys</h2>
<AccessKeyManagement
widgetId="user-access-key-management-widget"
tenant={process.env.DESCOPE_TENANT_ID!}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/sdks/react-sdk/examples/app/ManageAudit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ const ManageAudit = () => {
<h2>Manage Audit</h2>
<AuditManagement
widgetId="audit-management-widget"
tenant={process.env.DESCOPE_TENANT_ID}
tenant={process.env.DESCOPE_TENANT_ID!}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/sdks/react-sdk/examples/app/ManageRoles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ const ManageRoles = () => {
<h2>Manage Roles</h2>
<RoleManagement
widgetId="role-management-widget"
tenant={process.env.DESCOPE_TENANT_ID}
tenant={process.env.DESCOPE_TENANT_ID!}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/sdks/react-sdk/examples/app/ManageUsers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ const ManageUsers = () => {
<h2>Manage Users</h2>
<UserManagement
widgetId="user-management-widget"
tenant={process.env.DESCOPE_TENANT_ID}
tenant={process.env.DESCOPE_TENANT_ID!}
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ const MyApplicationsPortal = () => {
</div>
</header>
<h2>Applications Portal</h2>
<ApplicationsPortal widgetId="applications-portal-widget" />
<ApplicationsPortal
widgetId="applications-portal-widget"
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
/>
</>
);
};
Expand Down
4 changes: 4 additions & 0 deletions packages/sdks/react-sdk/examples/app/MyUserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const MyUserProfile = () => {
<h2>My Profile</h2>
<UserProfile
widgetId="user-profile-widget"
theme={process.env.DESCOPE_THEME as any}
styleId={process.env.DESCOPE_STYLE_ID}
debug={process.env.DESCOPE_DEBUG_MODE === 'true'}
logger={console}
onLogout={() => {
window.location.href = '/login';
}}
Expand Down
3 changes: 2 additions & 1 deletion packages/sdks/react-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"leaks": "bash ./scripts/gitleaks/gitleaks.sh",
"lint": "eslint '+(src|examples)/**/*.+(ts|tsx)' --fix",
"prepublishOnly": "npm run build",
"start": "npm run build && rollup -c rollup.config.app.mjs -w",
"start": "npx nx run react-sdk:build && rollup -c rollup.config.app.mjs -w",
"test": "jest"
},
"lint-staged": {
Expand All @@ -59,6 +59,7 @@
]
},
"dependencies": {
"@descope/sdk-helpers": "workspace:*",
"@descope/access-key-management-widget": "workspace:*",
"@descope/audit-management-widget": "workspace:*",
"@descope/role-management-widget": "workspace:*",
Expand Down
61 changes: 19 additions & 42 deletions packages/sdks/react-sdk/src/components/AccessKeyManagement.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,50 @@
import React, {
lazy,
Suspense,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import React, { lazy, Suspense, useImperativeHandle, useState } from 'react';
import Context from '../hooks/Context';
import { AccessKeyManagementProps } from '../types';
import withPropsMapping from './withPropsMapping';

// web-component code uses browser API, but can be used in SSR apps, hence the lazy loading
const AccessKeyManagementWC = lazy(async () => {
await import('@descope/access-key-management-widget');

return {
default: ({
projectId,
baseUrl,
baseStaticUrl,
innerRef,
tenant,
widgetId,
theme,
debug,
styleId,
}) => (
<descope-access-key-management-widget
project-id={projectId}
widget-id={widgetId}
base-url={baseUrl}
base-static-url={baseStaticUrl}
theme={theme}
tenant={tenant}
debug={debug}
style-id={styleId}
ref={innerRef}
/>
default: withPropsMapping(
React.forwardRef<HTMLElement>((props, ref) => (
<descope-access-key-management-widget ref={ref} {...props} />
)),
),
};
});

const AccessKeyManagement = React.forwardRef<
HTMLElement,
AccessKeyManagementProps
>(({ logger, tenant, theme, debug, widgetId }, ref) => {
>(({ logger, tenant, theme, debug, widgetId, styleId }, ref) => {
const [innerRef, setInnerRef] = useState(null);

useImperativeHandle(ref, () => innerRef);

const { projectId, baseUrl, baseStaticUrl } = React.useContext(Context);

useEffect(() => {
if (innerRef && logger) {
innerRef.logger = logger;
}
}, [innerRef, logger]);

return (
<Suspense fallback={null}>
<AccessKeyManagementWC
<Suspense fallback={null}>
<AccessKeyManagementWC
projectId={projectId}
widgetId={widgetId}
baseUrl={baseUrl}
baseStaticUrl={baseStaticUrl}
innerRef={setInnerRef}
tenant={tenant}
theme={theme}
debug={debug}
{...{
// attributes
'tenant.attr': tenant,
'theme.attr': theme,
'debug.attr': debug,
'styleId.attr': styleId,
// props
'logger.prop': logger,
}}
/>
</Suspense>
</Suspense>
);
});

Expand Down
55 changes: 17 additions & 38 deletions packages/sdks/react-sdk/src/components/ApplicationsPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
import React, {
lazy,
Suspense,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import React, { lazy, Suspense, useImperativeHandle, useState } from 'react';
import Context from '../hooks/Context';
import { ApplicationsPortalProps } from '../types';
import withPropsMapping from './withPropsMapping';

// web-component code uses browser API, but can be used in SSR apps, hence the lazy loading
const ApplicationsPortalWC = lazy(async () => {
await import('@descope/applications-portal-widget');

return {
default: ({
projectId,
baseUrl,
baseStaticUrl,
innerRef,
widgetId,
theme,
debug,
styleId,
}) => (
<descope-applications-portal-widget
project-id={projectId}
widget-id={widgetId}
base-url={baseUrl}
base-static-url={baseStaticUrl}
theme={theme}
debug={debug}
style-id={styleId}
ref={innerRef}
/>
default: withPropsMapping(
React.forwardRef<HTMLElement>((props, ref) => (
<descope-applications-portal-widget ref={ref} {...props} />
)),
),
};
});
Expand All @@ -47,24 +26,24 @@ const ApplicationsPortal = React.forwardRef<

const { projectId, baseUrl, baseStaticUrl } = React.useContext(Context);

useEffect(() => {
if (innerRef && logger) {
innerRef.logger = logger;
}
}, [innerRef, logger]);
return (
<Suspense fallback={null}>
<ApplicationsPortalWC
<Suspense fallback={null}>
<ApplicationsPortalWC
projectId={projectId}
widgetId={widgetId}
baseUrl={baseUrl}
baseStaticUrl={baseStaticUrl}
innerRef={setInnerRef}
theme={theme}
debug={debug}
styleId={styleId}
{...{
// attributes
'theme.attr': theme,
'debug.attr': debug,
'styleId.attr': styleId,
// props
'logger.prop': logger,
}}
/>
</Suspense>
</Suspense>
);
});

Expand Down
Loading

0 comments on commit efd6833

Please sign in to comment.