Skip to main content

Install Templ with one prompt

Use your app's wallet connection. The prompt adds Help and saves failure details for users to share.

Get ready​

  • Open your app's code in your AI coding tool.
  • Know the exact HTTPS site where you will test.

You do not need a ID to start. The tool can also use one already in your app's config.

Connect your app In testing

Add and test​

  1. Choose Copy install prompt below.
  2. Paste it into your coding tool inside your app's code.

Connect is in testing. Your tool prepares the integration with support off.

Review the support module, Help button, failure handlers and wallet updates the tool adds. For a known workspace ID, its manager approves each exact HTTPS origin in Widget. Send a test question. Reply from your inbox and check the reply in your app. When Check my install is available, use it to run the workspace's install check.

Read the prompt​

Read the prompt
Add Templ support chat to this app. Reuse the wallet connection the app already has.

Templ loads from a hosted ES module. There is no npm package. Don't install one or copy the hosted module into the repo.

Before you change code:
- Read https://usetempl.com/agents.md and the widget guide it links.
- Check https://usetempl.com/api/status. If support.widgetVerified is false, build the integration behind a config switch. Keep the switch off by default and tell me.
- Find the wallet library. EVM apps usually use wagmi, viem, ethers, RainbowKit, ConnectKit, Reown AppKit, Privy or Dynamic.
- Find the app shell and its Help or Support button. Find its sign-out and content security policy.

Install:
1. Add templ-support.ts with the code below. Use .js for a JavaScript app. In TypeScript, save https://usetempl.com/support-widget.d.ts next to it. Add that file to tsconfig "include".
2. Read the workspace ID from the app's public config. Follow the app's naming, such as NEXT_PUBLIC_TEMPL_WORKSPACE or VITE_TEMPL_WORKSPACE. If no workspace ID is configured, start Connect:
   - Check https://usetempl.com/api/status. If support.connectVerified is false, build with support off and tell me "Connect is not available yet."
   - Detect the app's exact HTTPS origins from its config. Origins have no path, query, fragment or trailing slash.
   - POST https://community.usetempl.com/connect/start with JSON { origins: [...] }. Add appName only when the app has a name. Use only origins you checked. Send no auth token.
   - Read code, connectUrl, pollToken, expiresAt and interval from the response.
   - Show me only connectUrl and code. Ask me to open that one link and sign in with my wallet.
   - I confirm the app and origins there. I can create a workspace or choose one I manage.
   - Keep pollToken only in memory. Never print it, log it, save it to a file or commit it.
   - POST https://community.usetempl.com/connect/poll with JSON { pollToken }. Wait at least interval seconds between polls. Honor Retry-After and each larger interval.
   - For pending, keep polling until expiresAt. For denied or expired, stop and report it.
   - For approved, check workspaceId is a UUID. Save only workspaceId in the app's public config, then continue.
   - If Connect fails, keep support off and report the error. Never invent an ID or ask me for one.
3. Call setSupportWallet every time the wallet connects, changes account or network, or disconnects:
   - EVM: setSupportWallet({ evm: provider }). Use the EIP-1193 provider of the wallet the user picked. With wagmi, use await connector.getProvider() for the connector from useAccount(). Don't use window.ethereum when the app picked another provider.
   - No wallet: setSupportWallet(null). Users can still ask without one.
4. Make the app's Help button call openSupport(workspaceId).
   If it rejects, show "Support couldn't load. Try again." Let the user press Help again to retry.
   If the app has no Help button, set SHOW_LAUNCHER to true.
   Call loadSupport(workspaceId) when the app shell mounts. Catch its rejection and show the same error with a Retry button.
   Make Retry call loadSupport(workspaceId) again. Hide the error after a successful mount.
   Clear the error and retry handler on sign-out or shell unmount. Ignore late failures after cleanup.
5. Find each handler where a wallet action fails.
   Handle wallet rejection, failed simulation, failed submission, and failure after a transaction was sent.
   Call captureSupportFailure(workspaceId, failure) there right away.
   Don't wait for it before showing the error. Catch its rejection so the app keeps working.
   The helper saves the details, time and wallet before loading the widget.
   A wallet change during loading must not replace that saved wallet.
   The helper keeps saved details after a failed load or capture.
   Get help retries capture before opening the chat. Never clear saved details after a failed attempt.
   Use only fields the app has:
   { network, transactionHash, app: { page, action, product, shown, error, appVersion, device, failure } }
   Show the app's error with a "Get help" button.
   Make that button call openSupport(workspaceId).
   Keep a Help link beside each pending transaction.
   Make that link call openSupport(workspaceId, { network, transactionHash }).
   Field rules:
   - network: "eip155:<chainId>".
   - page: starts with "/", path only, no query or fragment.
     Keep page at most 200 characters.
   - action: one of deposit, withdraw, claim, migrate, swap, stake, unstake, approve, bridge, other.
   - product: a label (at most 80 bytes), or an address with its network.
   - shown.value: the value the app showed, at most 40 characters.
     Use shown.metric to name what it measures.
     Set shown.asOf to an ISO 8601 time string.
   - error.code: at most 64 characters.
     Use only letters, digits and _ . : - in error.code.
     Keep error.message at most 300 bytes.
     Never put other people's wallet addresses, tokens, cookies, emails or keys in error.
   - appVersion and device: short strings, at most 60 bytes each.
   - failure.kind is required: one of rejected, simulation, submission, broadcast, other.
     Without it, nothing is saved.
     Optional failure.amount: a decimal string such as 25.5, at most 40 characters.
     No sign, exponent or leading zeros.
     Optional failure.asset: the asset's symbol (at most 60 bytes), or its address with its network.
   Any other field Templ can't read is left out with a console warning.
   The rest are still saved.
   The user still chooses what to share.
6. Call closeSupport() when the user signs out of the app. Also call it when the app shell unmounts. This clears saved details. On wallet disconnect, call only setSupportWallet(null). Keep the widget open across route changes.
7. If the app sets a content security policy, allow https://usetempl.com in script-src. Allow https://community.usetempl.com and wss://community.usetempl.com in connect-src. Allow https://usetempl.com in img-src. If the policy limits style-src, make styleNonce() return the page's style nonce. If script-src uses a nonce with strict-dynamic, give the script that bundles templ-support.ts the page's nonce. Don't add a policy the app doesn't have.
8. Match the widget to this app with appearance options on mountSupportWidget.
   Leave out defaults: theme "auto", radius "soft", and position "right".
   Auto follows the host page's theme, then the user's system setting.
   Set accent to "#rrggbb" or { light: "#rrggbb", dark: "#rrggbb" }.
   Filled buttons keep the chosen accent. The widget adjusts text, links, and focus rings for contrast.
   Radius accepts "square", "soft", or "round". Position accepts "right" or "left".
   Keep the host app's font.
   Call the returned widget's setAppearance(partial) when the app changes theme or accent.
   Each call changes only the options passed. It keeps the case and draft.

Rules:
- Don't add a second connect button or wallet modal.
- Never ask for a private key or recovery phrase.
- Support code must never start a transaction or a signature request.
  The widget requests its own sign-in signature only when the user chooses Sign in.
  The app's own user actions, such as Deposit, may request signatures and send transactions.
- Don't pass the app's login token, cookies or any data the user didn't choose to share.

```ts
// templ-support.ts: one Templ support widget for the whole app.
import type {
  SupportContextInput,
  SupportFailureCapture,
  SupportWallet,
  SupportWidget,
} from "https://usetempl.com/support-widget.js";

/** true only if the app has no Help button of its own. */
const SHOW_LAUNCHER = false;

/** The page's style nonce, needed only when the content security policy
 * limits style-src. Return it from where the app keeps it. */
function styleNonce(): string | undefined {
  return undefined;
}

let wallet: SupportWallet | null = null;
let widget: SupportWidget | undefined;
let loading: Promise<SupportWidget> | undefined;
let retries = 0;
type SavedFailure = {
  input: SupportContextInput;
  capture: Promise<SupportFailureCapture>;
};
const failures = new Set<SavedFailure>();
let capturing: Promise<void> | undefined;

/** Read the selected wallet now. These reads never connect or sign. */
async function snapshotWallet(): Promise<SupportFailureCapture["wallet"]> {
  const selected = wallet;
  try {
    if (selected?.evm) {
      const [accounts, chain] = await Promise.all([
        selected.evm.request({ method: "eth_accounts" }),
        selected.evm.request({ method: "eth_chainId" }),
      ]);
      const address = Array.isArray(accounts) ? accounts[0] : undefined;
      const chainId = typeof chain === "string" ? Number(chain) : NaN;
      if (
        typeof address === "string" && /^0x[0-9a-f]{40}$/i.test(address) &&
        !/^0x0{40}$/i.test(address) && Number.isSafeInteger(chainId) && chainId > 0
      ) return { network: "evm", address: address.toLowerCase() as `0x${string}`, chainId };
    } else if (selected?.solana) {
      const address = "accounts" in selected.solana
        ? selected.solana.accounts.find((account) =>
            !account.chains || account.chains.some((chain) => chain.startsWith("solana:")),
          )?.address
        : selected.solana.publicKey?.toBase58();
      if (address) return { network: "solana", address };
    }
  } catch {
    // An unreadable wallet stays a guest snapshot.
  }
  return null;
}

/** Keep each failure until the widget accepts it. Get help retries this queue. */
async function captureSavedFailures(mounted: SupportWidget): Promise<void> {
  if (capturing) return capturing;
  const attempt = (async () => {
    let failed = false;
    let reason: unknown;
    for (const saved of failures) {
      const capture = await saved.capture;
      if (widget !== mounted || !failures.has(saved)) throw new Error("Support was closed.");
      try {
        await mounted.captureFailure(saved.input, capture);
        failures.delete(saved);
      } catch (error) {
        failed = true;
        reason = error;
      }
    }
    if (failed) throw reason;
  })();
  capturing = attempt;
  try {
    await attempt;
  } finally {
    if (capturing === attempt) capturing = undefined;
  }
}

/** Loads and mounts the widget once. `workspace` is the Templ workspace ID. */
export function loadSupport(workspace: string): Promise<SupportWidget> {
  if (loading) return loading;
  const url = "https://usetempl.com/support-widget.js";
  const source = retries ? url + "?retry=" + retries : url;
  const attempt: Promise<SupportWidget> = import(
    /* webpackIgnore: true */ /* @vite-ignore */ source
  ).then(({ mountSupportWidget }) => {
    if (loading !== attempt) throw new Error("Support was closed.");
    const mounted: SupportWidget = mountSupportWidget({
      workspace,
      apiUrl: "https://community.usetempl.com",
      wallet,
      showLauncher: SHOW_LAUNCHER,
      styleNonce: styleNonce(),
    });
    widget = mounted;
    return mounted;
  });
  attempt.catch(() => {
    if (loading === attempt) {
      loading = undefined;
      retries++;
    }
  });
  loading = attempt;
  return attempt;
}

/** The app's Help button. Pass context only for something the user is
 * looking at, such as a pending deposit; they choose whether to share it. */
export async function openSupport(
  workspace: string,
  context?: SupportContextInput,
) {
  const mounted = await loadSupport(workspace);
  await captureSavedFailures(mounted).catch(() => {});
  if (widget !== mounted) throw new Error("Support was closed.");
  if (context) mounted.setContext(context);
  mounted.open();
}

/** Call from the app's handler when a wallet action fails.
 * It saves the failure with the wallet and network at that moment.
 * Nothing is sent until the user ticks it in the chat. */
export async function captureSupportFailure(
  workspace: string,
  failure: SupportContextInput,
) {
  if (!["rejected","simulation","submission","broadcast","other"].includes(failure.app?.failure?.kind ?? "")) {
    throw new Error("Add a failure kind.");
  }
  const capturedAt = new Date().toISOString();
  const saved = {
    input: structuredClone(failure),
    capture: snapshotWallet().then((wallet) => ({ capturedAt, wallet })),
  };
  failures.add(saved);
  const mounted = await loadSupport(workspace);
  await captureSavedFailures(mounted);
}

/** Call whenever the app's wallet connects, switches account or chain, or
 * disconnects. EVM: { evm: provider }. Solana: { solana: wallet }. None: null. */
export function setSupportWallet(next: SupportWallet | null) {
  wallet = next;
  widget?.setWallet(next);
}

/** Call on app sign-out and when the app shell unmounts. */
export function closeSupport() {
  loading = undefined;
  capturing = undefined;
  failures.clear();
  widget?.destroy();
  widget = undefined;
}
```

When you're done, list the origins approved through Connect. For a known workspace ID, its manager approves each exact HTTPS origin in Widget. For local testing, use your host's HTTPS preview or an HTTPS tunnel to your local server. Approve that exact HTTPS origin in Widget. Open the app at that URL before testing.

Then test it:
- Open Help or the support launcher and send a message without a wallet.
- Reply from https://usetempl.com/inbox. Check that the reply shows in the app.
- Connect a wallet, switch accounts and disconnect.
- Make a wallet action fail, then press Get help.
- Check the unticked failure rows under Details from this app.
- Change wallets while the widget loads. Check that the failure keeps the wallet from that moment.
- Block the widget module once, then press Get help to retry. Check that the saved details appear.
- Without a Help button, block the first widget load. Check the error and press Retry to load the launcher.
Tell me which checks passed and what's still missing.

Check my install In testing

Review the widget reference for fields, wallet updates and site permissions. Set up your team after connecting the app. Fix install problems if the widget cannot open or a does not arrive.

Read the tested stacks​

SetupRun dateLocal checks
Next.js 16.3.6, wagmi 2.19.5, RainbowKit 2.2.11, viem2026-09-29Type check, build and 6 integration tests
Next.js 16.3.6, Solana wallet-adapter 0.15.40 In testing2026-09-29Type check, build and 9 integration tests

These dated runs checked the listed stacks locally.

They do not verify the current prompt or a hosted install.

Guest messages, inbox replies, real wallet changes and failure rows still need hosted checks.

Other stacks: not tested yet.

These local checks do not prove hosted availability. Run the full test list on your approved HTTPS site before inviting users.

Remove Templ​

Remove the Help handler and support module calls from your app. Remove the workspace ID from its public config. Stop loading the hosted widget module. Remove its CSP entries if nothing else needs them. Your workspace keeps case data under its data rules.

Ask an install question​

Use Templ’s widget to ask an install question.

Read what users see before sending a real case. Never share a private key or recovery phrase.

In testing means the feature is built and not yet open to every workspace.