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
27 changes: 27 additions & 0 deletions packages/connect-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ComponentFormContainer userId={userId} />

// After (recommended)
<ComponentFormContainer externalUserId={userId} />
```

# [1.2.1] - 2025-06-07

- Fixing the SelectApp component to properly handle controlled values when not found in search results
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/connect-react",
"version": "1.2.1",
"version": "1.3.0",
"description": "Pipedream Connect library for React",
"files": [
"dist"
Expand Down
14 changes: 12 additions & 2 deletions packages/connect-react/src/components/ComponentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import type {
import { InternalComponentForm } from "./InternalComponentForm";

export type ComponentFormProps<T extends ConfigurableProps, U = ConfiguredProps<T>> = {
userId: string;
/**
* Your end user ID, for whom you're configuring the component.
*/
externalUserId?: string;
/**
* @deprecated Use `externalUserId` instead.
*/
userId?: string;
component: V1Component<T>;
configuredProps?: U; // XXX value?
disableQueryDisabling?: boolean;
Expand All @@ -22,7 +29,10 @@ export type ComponentFormProps<T extends ConfigurableProps, U = ConfiguredProps<
hideOptionalProps?: boolean;
sdkResponse?: unknown | undefined;
enableDebugging?: boolean;
};
} & (
| { externalUserId: string; userId?: never }
| { userId: string; externalUserId?: never }
);

export function ComponentForm<T extends ConfigurableProps>(props: ComponentFormProps<T>) {
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/connect-react/src/components/ControlApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,6 +29,7 @@ type ControlAppProps = {

export function ControlApp({ app }: ControlAppProps) {
const client = useFrontendClient();
const { externalUserId } = useFormContext();
const formFieldCtx = useFormFieldContext<ConfigurablePropApp>();
const {
id, prop, value, onChange,
Expand Down Expand Up @@ -73,6 +75,7 @@ export function ControlApp({ app }: ControlAppProps) {
refetch: refetchAccounts,
} = useAccounts(
{
externalUserId,
app: app.name_slug,
oauth_app_id: oauthAppId,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type RemoteOptionsContainerProps = {
export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerProps) {
const client = useFrontendClient();
const {
userId,
externalUserId,
component,
configurableProps,
configuredProps,
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 32 additions & 7 deletions packages/connect-react/src/hooks/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import {
Observation, SdkError,
} from "../types";
import { resolveUserId } from "../utils/resolve-user-id";

export type DynamicProps<T extends ConfigurableProps> = { id: string; configurableProps: T; }; // TODO

Expand All @@ -39,6 +40,8 @@ export type FormContext<T extends ConfigurableProps> = {
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;
};
Expand Down Expand Up @@ -77,8 +80,29 @@ export const FormContextProvider = <T extends ConfigurableProps>({
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 [
Expand Down Expand Up @@ -134,7 +158,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
setReloadPropIdx,
] = useState<number>();
const componentReloadPropsInput: ReloadComponentPropsOpts = {
userId,
externalUserId: resolvedExternalUserId,
componentId,
configuredProps,
dynamicPropsId: dynamicProps?.id,
Expand Down Expand Up @@ -349,14 +373,14 @@ export const FormContextProvider = <T extends ConfigurableProps>({
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
Expand Down Expand Up @@ -550,7 +574,8 @@ export const FormContextProvider = <T extends ConfigurableProps>({
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,
Expand Down
29 changes: 29 additions & 0 deletions packages/connect-react/src/utils/resolve-user-id.ts
Original file line number Diff line number Diff line change
@@ -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");
};
Loading