> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Activation form

> Connect form where users provide credentials and configuration data.

The activation form is what a user fills in to connect your integration. Use `BaseActivationForm` for the dialog chrome, submit button, and loading states.

## Basic form

```tsx theme={null}
'use client';

import { useState } from 'react';
import type { ActivationFormProps } from '@ekanos/sdk';
import { BaseActivationForm } from '@ekanos/sdk/components';
import { useActivateIntegration } from '@ekanos/sdk/hooks';
import {
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@ekanos/ui/form';
import { Input } from '@ekanos/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

import { ACME_API_KEY, ACME_MERCHANT_ID } from './config';

const ActivationSchema = z.object({
  [ACME_API_KEY]: z.string().min(1, 'Required'),
  [ACME_MERCHANT_ID]: z.string().min(1, 'Required'),
});

export function AcmePaymentsActivationForm({
  accountId,
  sourceId,
  onSuccess,
  onCancel,
  isLoading: externalLoading = false,
  inline = false,
}: ActivationFormProps) {
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  const form = useForm({
    resolver: zodResolver(ActivationSchema),
    defaultValues: { [ACME_API_KEY]: '', [ACME_MERCHANT_ID]: '' },
  });

  const activate = useActivateIntegration();
  const isLoading = externalLoading || activate.isPending;

  const handleSubmit = async (data: Record<string, unknown>) => {
    setError(null);
    try {
      await activate.mutateAsync({
        accountId,
        sourceId,
        integrationSlug: 'acme-payments',
        activationData: data,
      });
      setSuccess(true);
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : 'Could not connect.');
    }
  };

  return (
    <BaseActivationForm
      isOpen
      inline={inline}
      onClose={() => onCancel?.()}
      title="Connect Acme Payments"
      description="Bring volume, activity and refunds into your dashboard."
      form={form}
      onSubmit={handleSubmit}
      isLoading={isLoading}
      error={error}
      success={success}
      successTitle="Acme Payments is connected"
      onSuccessClose={() =>
        onSuccess?.({
          success: true,
          configData: { merchantId: form.getValues(ACME_MERCHANT_ID) },
          externalId: form.getValues(ACME_MERCHANT_ID),
        })
      }
      submitButtonText="Connect Acme Payments"
      submitButtonLoadingText="Connecting…"
    >
      <FormField
        control={form.control}
        name={ACME_API_KEY}
        render={({ field }) => (
          <FormItem>
            <FormLabel>API key</FormLabel>
            <FormControl>
              <Input type="password" {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />

      <FormField
        control={form.control}
        name={ACME_MERCHANT_ID}
        render={({ field }) => (
          <FormItem>
            <FormLabel>Merchant ID</FormLabel>
            <FormControl>
              <Input {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
    </BaseActivationForm>
  );
}
```

## Props

`ActivationFormProps` is `IntegrationComponentProps` plus:

```ts theme={null}
{
  onSuccess?: (result: ActivationResult) => void;
  onCancel?: () => void;
  isLoading?: boolean;
  inline?: boolean;        // render inline instead of in a dialog
  teamAccountId?: string;  // the team account the user is activating within
}
```

`IntegrationComponentProps` is `{ accountId, sourceId?, integrationConfig? }` plus exactly one of `productId` or `productSlug`.

* **`inline`** — pass through; without it the form mounts its own dialog, so a page that embeds it ends up with nested dialogs
* **`onSuccess`** — call it with an `ActivationResult` (`{ success, configData?, externalId?, errorMessage?, metadata? }`) once activation succeeds. Values you want back on the client go in `configData`
* **`onCancel`** — call it when the user cancels

## Secret naming

Every string field you submit becomes a secret:

```tsx theme={null}
const data = {
  apiKey: 'user-entered', // String → secret
  merchantId: 'user-entered', // String → secret
  retryCount: 3, // Number → NOT a secret
  advanced: {}, // Object → NOT a secret
};

// Server-side:
const apiKey = await ctx.secrets.get('apiKey');
const merchantId = await ctx.secrets.get('merchantId');
```

**The field name is the secret name.** Put names in a constant to avoid drift:

```ts theme={null}
// src/config.ts
export const ACME_API_KEY = 'acme_api_key';
export const ACME_MERCHANT_ID = 'acme_merchant_id';
```

Use both in the form and in handlers.

## OAuth variant

For an integration whose definition declares `oauth`, `OAuthActivationForm` renders a "Connect with …" button that sends the browser to the host's connect route, then activates once the OAuth redirect returns with a live connection. Point it at the hosted routes with the helpers from `@ekanos/sdk/hooks` rather than spelling the paths yourself:

```tsx theme={null}
'use client';

import type { ActivationFormProps } from '@ekanos/sdk';
import { OAuthActivationForm } from '@ekanos/sdk/components';
import {
  createPartnerConnectionCheck,
  partnerOAuthConnectPath,
  partnerOAuthErrorParam,
} from '@ekanos/sdk/hooks';

const SLUG = 'acme-payments';

export function AcmeOAuthActivationForm(props: ActivationFormProps) {
  return (
    <OAuthActivationForm
      {...props}
      config={{
        slug: SLUG,
        displayName: 'Acme',
        title: 'Connect Acme',
        description: 'Sign in with Acme to bring your payments into Ekanos.',
        successMessage: 'Your Acme account is connected.',
        connectPath: partnerOAuthConnectPath(SLUG),
      }}
      checkConnection={createPartnerConnectionCheck(SLUG)}
      errorParam={partnerOAuthErrorParam(SLUG)}
    />
  );
}
```

`config` also accepts `successTitle`, `connectedBanner`, `connectButtonText`, `activateButtonText`, `externalIdPrefix`, and `autoActivateOnConnect` (default `true`; set `false` when the user should review post-connect settings before activating). Pass `connectPath` explicitly — its default points at a per-integration route that partner integrations do not have. `useOAuthConnectionStatus(accountId, { connectionKey, checkConnection, errorParam })` is the underlying hook if you build your own form. See [OAuth](/sdk/surfaces/oauth).

## Declaration

```ts theme={null}
components: {
  activationForm: AcmePaymentsActivationForm,
}
```

Forms are optional. Omit it and the host renders a generic one — a plain field per value it needs.

## Error handling

Errors from `activate.mutateAsync` become the `error` prop:

```ts theme={null}
try {
  await activate.mutateAsync(/* ... */);
} catch (cause) {
  setError(cause instanceof Error ? cause.message : 'Could not connect.');
}
```

## Troubleshooting

### No IntegrationActivationProvider is mounted

Submitting rejects with `No IntegrationActivationProvider is mounted. The host application must bind its activation server actions (see @ekanos/sdk/hooks IntegrationActivationProvider) above any component that activates integrations.` You're rendering outside the host (Storybook, your own app). Mount `IntegrationActivationProvider` from `@ekanos/sdk/hooks` with `activate` and `deactivate` functions:

```tsx theme={null}
const mockActions = {
  activate: async () => ({ success: true }),
  deactivate: async () => ({ success: true }),
};

<IntegrationActivationProvider actions={mockActions}>
  <AcmePaymentsActivationForm {...props} />
</IntegrationActivationProvider>;
```

### Nested dialogs

Forgot to pass `inline`? The form mounts its own `Dialog`, causing nesting. Always pass through: `inline={inline}`.

## Next steps

* [Secrets](/sdk/surfaces/secrets) — credential management
* [Widgets](/sdk/surfaces/widgets) — what renders after activation


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.