Skip to content
Draft
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
8 changes: 8 additions & 0 deletions src/components/Configure/InstallIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export interface InstallIntegrationProps {
* @experimental
*/
fieldMapping?: FieldMapping;
/**
* When true, reauthenticating an existing connection also restarts subscribe setup for the
* installation if it never finished. Off by default. The consumer sees no difference; the
* reconnect happens behind the reauthentication flow.
*/
resubscribeOnReAuth?: boolean;
onInstallSuccess?: (installationId: string, config: Config) => void;
onUpdateSuccess?: (installationId: string, config: Config) => void;
onUninstallSuccess?: (installationId: string) => void;
Expand Down Expand Up @@ -86,6 +92,7 @@ const InstallIntegrationContent = ({
onUpdateSuccess,
onUninstallSuccess,
fieldMapping,
resubscribeOnReAuth,
variant,
}: InstallIntegrationInternalProps) => {
const { installation, isPending: isInstallationPending } = useInstallation();
Expand Down Expand Up @@ -173,6 +180,7 @@ const InstallIntegrationContent = ({
onUpdateSuccess={onUpdateSuccess}
onUninstallSuccess={onUninstallSuccess}
fieldMapping={fieldMapping}
resubscribeOnReAuth={resubscribeOnReAuth}
resetComponent={reset}
>
<ConnectionsProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAmpersandProviderProps } from "src/context/AmpersandContextProvider"
import { useConnections } from "src/context/ConnectionsContextProvider";
import { useUpdateConnectionMutation } from "src/hooks/mutation/useUpdateConnectionMutation";
import { useProvider } from "src/hooks/useProvider";
import { useResubscribeOnReauth } from "src/hooks/useResubscribeOnReauth";
import { handleServerError } from "src/utils/handleServerError";

import { FieldHeader } from "../../fields/FieldHeader";
Expand All @@ -32,9 +33,13 @@ export function UpdateApiKeyConnect({ provider }: { provider?: string }) {

const resetSuccessConnect = () => setSuccessConnect(false);

const startResubscribe = useResubscribeOnReauth();

const handleSuccessConnect = () => {
setSuccessConnect(true);
setError(null);
// Opt-in only; no-ops unless the builder set resubscribeOnReAuth.
startResubscribe();
};

const error = updateError?.message || localError || null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAmpersandProviderProps } from "src/context/AmpersandContextProvider"
import { useConnections } from "src/context/ConnectionsContextProvider";
import { useUpdateConnectionMutation } from "src/hooks/mutation/useUpdateConnectionMutation";
import { useProvider } from "src/hooks/useProvider";
import { useResubscribeOnReauth } from "src/hooks/useResubscribeOnReauth";
import { handleServerError } from "src/utils/handleServerError";

import { FieldHeader } from "../../fields/FieldHeader";
Expand All @@ -32,9 +33,13 @@ export function UpdateBasicAuthConnect({ provider }: { provider?: string }) {

const resetSuccessConnect = () => setSuccessConnect(false);

const startResubscribe = useResubscribeOnReauth();

const handleSuccessConnect = () => {
setSuccessConnect(true);
setError(null);
// Opt-in only; no-ops unless the builder set resubscribeOnReAuth.
startResubscribe();
};

const error = updateError?.message || localError || null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAmpersandProviderProps } from "src/context/AmpersandContextProvider"
import { useConnections } from "src/context/ConnectionsContextProvider";
import { useUpdateConnectionMutation } from "src/hooks/mutation/useUpdateConnectionMutation";
import { useProvider } from "src/hooks/useProvider";
import { useResubscribeOnReauth } from "src/hooks/useResubscribeOnReauth";
import { handleServerError } from "src/utils/handleServerError";

import { FieldHeader } from "../../fields/FieldHeader";
Expand Down Expand Up @@ -37,9 +38,13 @@ export function UpdateClientCredentialsConnect({
setSuccessConnect(false);
};

const startResubscribe = useResubscribeOnReauth();

const handleSuccessConnect = () => {
setSuccessConnect(true);
setError(null);
// Opt-in only; no-ops unless the builder set resubscribeOnReAuth.
startResubscribe();
};

const explicitScopesRequired =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from "src/components/ui-base/Button";
import { useConnections } from "src/context/ConnectionsContextProvider";
import { useUpdateOauthConnectMutation } from "src/hooks/mutation/useUpdateOauthConnectMutation";
import { useProvider } from "src/hooks/useProvider";
import { useResubscribeOnReauth } from "src/hooks/useResubscribeOnReauth";
import { handleServerError } from "src/utils/handleServerError";

import { FieldHeader } from "../../fields/FieldHeader";
Expand Down Expand Up @@ -63,10 +64,14 @@ export function UpdateOauthConnect({ provider }: { provider?: string }) {
setSuccessConnect(false);
};

const startResubscribe = useResubscribeOnReauth();

const handleSuccessConnect = () => {
setSuccessConnect(true);
setError(null);
setUrl(null);
// Opt-in only; no-ops unless the builder set resubscribeOnReAuth.
startResubscribe();
};

const error = updateOauthConnectError?.message || localError || null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ interface InstallIntegrationContextValue {
isIntegrationDeleted: boolean;
setIntegrationDeleted: () => void;
fieldMapping?: FieldMapping;
/** Builder opt-in: restart subscribe setup after a successful reauthentication. */
resubscribeOnReAuth: boolean;
resetComponent: () => void;
}
// Create a context to pass down the props
Expand All @@ -59,6 +61,7 @@ export const InstallIntegrationContext =
onUninstallSuccess: undefined,
isIntegrationDeleted: false,
setIntegrationDeleted: () => {},
resubscribeOnReAuth: false,
resetComponent: () => {},
});

Expand All @@ -84,6 +87,7 @@ interface InstallIntegrationProviderProps {
onUpdateSuccess?: (installationId: string, config: Config) => void;
onUninstallSuccess?: (installationId: string) => void;
fieldMapping?: FieldMapping;
resubscribeOnReAuth?: boolean;
resetComponent: () => void;
}

Expand All @@ -99,6 +103,7 @@ export function InstallIntegrationProvider({
onUpdateSuccess,
onUninstallSuccess,
fieldMapping,
resubscribeOnReAuth = false,
resetComponent,
}: InstallIntegrationProviderProps) {
const { data: integrations } = useListIntegrationsQuery();
Expand Down Expand Up @@ -194,6 +199,7 @@ export function InstallIntegrationProvider({
isIntegrationDeleted,
setIntegrationDeleted,
fieldMapping,
resubscribeOnReAuth,
resetComponent,
}),
[
Expand All @@ -211,6 +217,7 @@ export function InstallIntegrationProvider({
isIntegrationDeleted,
setIntegrationDeleted,
fieldMapping,
resubscribeOnReAuth,
resetComponent,
],
);
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/mutation/useResubscribeInstallationMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMutation } from "@tanstack/react-query";
import { useAPI } from "services/api";

export interface ResubscribeInstallationRequest {
projectIdOrName: string;
integrationId: string;
installationId: string;
}

/**
* Asks the server to restart subscribe setup for an installation.
*
* The server decides whether anything needs doing and returns 200 either way, so this is safe to
* call without inspecting subscription state first. The work runs asynchronously; there is nothing
* to await beyond the request being accepted.
*/
export const useResubscribeInstallationMutation = () => {
const getAPI = useAPI();

return useMutation({
mutationKey: ["resubscribeInstallation"],
mutationFn: async (request: ResubscribeInstallationRequest) => {
const api = await getAPI();

return api.installationApi.resubscribeInstallation(request);
},
});
};
48 changes: 48 additions & 0 deletions src/hooks/useResubscribeOnReauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback } from "react";
import { useInstallIntegrationProps } from "context/InstallIIntegrationContextProvider/InstallIntegrationContextProvider";
import { useProjectQuery } from "src/hooks/query";

import { useResubscribeInstallationMutation } from "./mutation/useResubscribeInstallationMutation";

/**
* Returns a function to call once a reauthentication has persisted new credentials, which restarts
* subscribe setup for the installation.
*
* Reconnecting is opt-in: it does nothing unless the builder passed `resubscribeOnReAuth` to
* `InstallIntegration`. It also does nothing without an installation in context, since the
* update-connection UI can render inside `ConnectProvider`, where there is no installation to
* reconnect.
*
* The call is deliberately fire-and-forget and invisible to the consumer. The credential update has
* already succeeded by this point, so a failure here must not be surfaced as a failed
* reauthentication; builders see failures through the operations API and the subscribe error
* notification instead.
*/
export function useResubscribeOnReauth() {
const { resubscribeOnReAuth, integrationId, installation } =
useInstallIntegrationProps();
const { projectIdOrName } = useProjectQuery();
const { mutate } = useResubscribeInstallationMutation();

const installationId = installation?.id;

return useCallback(() => {
if (!resubscribeOnReAuth) return;
if (!projectIdOrName || !integrationId || !installationId) return;

mutate(
{ projectIdOrName, integrationId, installationId },
{
onError: (error) => {
console.error("Failed to start subscription reconnect", error);
},
},
);
}, [
resubscribeOnReAuth,
projectIdOrName,
integrationId,
installationId,
mutate,
]);
}
Loading