diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 91eb9e0021eed..354ab2afcd45b 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -2,6 +2,33 @@ # Changelog +# [1.3.0] - 2025-06-10 + +## Added + +- Support for `externalUserId` param across all components as the preferred way to identify users +- Backward compatibility with existing `userId` parameter + +## Changed + +- All internal SDK calls now use `externalUserId` parameter for consistency with backend SDK + +## Deprecated + +- `userId` parameter in favor of `externalUserId` (existing code continues to work with console warnings) + +## Migration + +Replace `userId` with `externalUserId` in component props: + +```typescript +// Before + + +// After (recommended) + +``` + # [1.2.1] - 2025-06-07 - Fixing the SelectApp component to properly handle controlled values when not found in search results diff --git a/packages/connect-react/package.json b/packages/connect-react/package.json index 84c28e1962c22..412efcbabae97 100644 --- a/packages/connect-react/package.json +++ b/packages/connect-react/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/connect-react", - "version": "1.2.1", + "version": "1.3.0", "description": "Pipedream Connect library for React", "files": [ "dist" diff --git a/packages/connect-react/src/components/ComponentForm.tsx b/packages/connect-react/src/components/ComponentForm.tsx index 47c19f9cac8f2..2ebe0b2dbaabf 100644 --- a/packages/connect-react/src/components/ComponentForm.tsx +++ b/packages/connect-react/src/components/ComponentForm.tsx @@ -10,7 +10,14 @@ import type { import { InternalComponentForm } from "./InternalComponentForm"; export type ComponentFormProps> = { - userId: string; + /** + * Your end user ID, for whom you're configuring the component. + */ + externalUserId?: string; + /** + * @deprecated Use `externalUserId` instead. + */ + userId?: string; component: V1Component; configuredProps?: U; // XXX value? disableQueryDisabling?: boolean; @@ -22,7 +29,10 @@ export type ComponentFormProps(props: ComponentFormProps) { return ( diff --git a/packages/connect-react/src/components/ControlApp.tsx b/packages/connect-react/src/components/ControlApp.tsx index cfdf0a063207e..0535ce881c00e 100644 --- a/packages/connect-react/src/components/ControlApp.tsx +++ b/packages/connect-react/src/components/ControlApp.tsx @@ -2,6 +2,7 @@ import Select, { components as ReactSelectComponents } from "react-select"; import { useFrontendClient } from "../hooks/frontend-client-context"; import { useAccounts } from "../hooks/use-accounts"; import { useFormFieldContext } from "../hooks/form-field-context"; +import { useFormContext } from "../hooks/form-context"; import { useCustomize } from "../hooks/customization-context"; import type { BaseReactSelectProps } from "../hooks/customization-context"; import { useMemo } from "react"; @@ -28,6 +29,7 @@ type ControlAppProps = { export function ControlApp({ app }: ControlAppProps) { const client = useFrontendClient(); + const { externalUserId } = useFormContext(); const formFieldCtx = useFormFieldContext(); const { id, prop, value, onChange, @@ -73,6 +75,7 @@ export function ControlApp({ app }: ControlAppProps) { refetch: refetchAccounts, } = useAccounts( { + externalUserId, app: app.name_slug, oauth_app_id: oauthAppId, }, diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index d606bee52f6c0..5dd526f79265b 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -13,7 +13,7 @@ export type RemoteOptionsContainerProps = { export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerProps) { const client = useFrontendClient(); const { - userId, + externalUserId, component, configurableProps, configuredProps, @@ -60,7 +60,7 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP configuredPropsUpTo[prop.name] = configuredProps[prop.name]; } const componentConfigureInput: ComponentConfigureOpts = { - userId, + externalUserId, page, prevContext: context, componentId: component.key, diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index e78ba540919a3..d516615bda0aa 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -16,6 +16,7 @@ import { import { Observation, SdkError, } from "../types"; +import { resolveUserId } from "../utils/resolve-user-id"; export type DynamicProps = { id: string; configurableProps: T; }; // TODO @@ -39,6 +40,8 @@ export type FormContext = { setConfiguredProp: (idx: number, value: unknown) => void; // XXX type safety for value (T will rarely be static right?) setSubmitting: (submitting: boolean) => void; submitting: boolean; + externalUserId: string; + /** @deprecated Use externalUserId instead */ userId: string; enableDebugging?: boolean; }; @@ -77,8 +80,29 @@ export const FormContextProvider = ({ const id = useId(); const { - component, configuredProps: __configuredProps, propNames, userId, sdkResponse, enableDebugging, + component, configuredProps: __configuredProps, propNames, externalUserId, userId, sdkResponse, enableDebugging, } = formProps; + + // Resolve user ID with deprecation warning + const { + resolvedId: resolvedExternalUserId, warningType, + } = useMemo(() => resolveUserId(externalUserId, userId), [ + externalUserId, + userId, + ]); + + // Show deprecation warnings in useEffect to avoid render side effects + useEffect(() => { + if (process.env.NODE_ENV !== "production") { + if (warningType === "both") { + console.warn("[connect-react] Both externalUserId and userId provided. Using externalUserId. Please remove userId to avoid this warning."); + } else if (warningType === "deprecated") { + console.warn("[connect-react] userId is deprecated. Please use externalUserId instead."); + } + } + }, [ + warningType, + ]); const componentId = component.key; const [ @@ -134,7 +158,7 @@ export const FormContextProvider = ({ setReloadPropIdx, ] = useState(); const componentReloadPropsInput: ReloadComponentPropsOpts = { - userId, + externalUserId: resolvedExternalUserId, componentId, configuredProps, dynamicPropsId: dynamicProps?.id, @@ -349,14 +373,14 @@ export const FormContextProvider = ({ const [ prevUserId, setPrevUserId, - ] = useState(userId) + ] = useState(resolvedExternalUserId) useEffect(() => { - if (prevUserId !== userId) { + if (prevUserId !== resolvedExternalUserId) { updateConfiguredProps({}); - setPrevUserId(userId) + setPrevUserId(resolvedExternalUserId) } }, [ - userId, + resolvedExternalUserId, ]); // maybe should take prop as first arg but for text inputs didn't want to compute index each time @@ -550,7 +574,8 @@ export const FormContextProvider = ({ id, isValid: !Object.keys(errors).length, // XXX want to expose more from errors props: formProps, - userId, + externalUserId: resolvedExternalUserId, + userId: resolvedExternalUserId, // Keep for backward compatibility component, configurableProps, configuredProps, diff --git a/packages/connect-react/src/utils/resolve-user-id.ts b/packages/connect-react/src/utils/resolve-user-id.ts new file mode 100644 index 0000000000000..ee269ba96a168 --- /dev/null +++ b/packages/connect-react/src/utils/resolve-user-id.ts @@ -0,0 +1,29 @@ +/** + * Resolves user ID from either externalUserId or userId parameters + * Prefers externalUserId and returns both the resolved value and warning info + */ +export const resolveUserId = ( + externalUserId?: string, + userId?: string, +): { resolvedId: string; warningType?: "both" | "deprecated" } => { + if (externalUserId !== undefined && externalUserId !== "") { + if (userId !== undefined && userId !== "") { + return { + resolvedId: externalUserId, + warningType: "both", + }; + } + return { + resolvedId: externalUserId, + }; + } + + if (userId !== undefined && userId !== "") { + return { + resolvedId: userId, + warningType: "deprecated", + }; + } + + throw new Error("Either externalUserId or userId must be provided"); +};