# Check a charity by ABN in a donation form

What you build: a React donation form. The donor types the charity's ABN; the field looks it up in the ACNC register through your relay and shows the legal name; the chosen charity's ABN and name travel to your server in hidden fields, so a gift is only ever recorded against a registered charity. "No charity found" and "lookup unavailable" are different answers.

## Before you start: a development app, then a production app

1. `npx comms-id init` makes a development key in `.env.local` and prints its public key. In the Companion App, register it as a development app, enable **Charity** for it, and put the app id in `.env.local` as `COMMS_ID_CLIENT_ID`. This key reaches only the TEST service (`https://api-test.comms.id`): the same API, errors and authentication as LIVE, with synthetic replies.
2. When it works in TEST, `npx comms-id key --worker <your worker>` makes the production key straight into your server's `COMMS_ID_PRIVATE_JWK` secret and prints the public key; register it as a production app with Charity enabled. Going live changes only the environment and the key: nothing in the code below.

A `.env.local` never holds a production key, and a browser never holds any key.

## The form

`pnpm add @comms-id/charity @comms-id/charity-react react`

`DonationForm.tsx`:

```tsx
// A donation form that checks the charity's ABN against the ACNC register as the donor types.
// The browser calls your own relay route; the chosen charity's ABN and legal name go to your
// server in hidden fields, so the gift is recorded against a registered charity.
import type { CharityClient } from "@comms-id/charity";
import { createCharityBrowserClient } from "@comms-id/charity/browser";
import { CharityLookup, type CharityRecord } from "@comms-id/charity-react";
import { useState } from "react";

export const relayClient = (): CharityClient => createCharityBrowserClient({ relayUrl: "/api/comms-id/charity" });

export function DonationForm({ client = relayClient() }: { readonly client?: CharityClient }) {
  const [charity, setCharity] = useState<CharityRecord | null>(null);
  return (
    <form action="/donate" method="post">
      <CharityLookup client={client} label="Charity ABN" onSelect={(record) => setCharity(record)} required />
      <input name="charity_abn" type="hidden" value={charity?.data.abn ?? ""} />
      <input name="charity_name" type="hidden" value={charity?.data.legalName ?? ""} />
      <p>{charity ? `Donating to ${charity.data.legalName}` : "Enter the charity's ABN to continue."}</p>
      <label>
        Amount (AUD) <input min="1" name="amount" required type="number" />
      </label>
      <button disabled={charity === null} type="submit">
        Donate
      </button>
    </form>
  );
}
```

`CharityLookup` is the finished field: it refuses anything that is not eleven digits before sending, looks a complete ABN up at once, shows one line for the found charity with the ACNC attribution, and offers "Try again" on an outage. `onSelect` gives you the whole register record.

## Your relay: one route, one signer

The browser calls your own server at `/api/comms-id/<product>/<operation>`; your server signs each call as your registered app with `@comms-id/relay`, so the page holds no credential. The relay forwards only the operations the product descriptors list, and never the user's cookies.

`pnpm add @comms-id/relay @comms-id/address @comms-id/asx @comms-id/charity @comms-id/logo @comms-id/sanctions`

`app/api/comms-id/[product]/[operation]/route.ts` (Next.js; `@comms-id/relay/hono` is the same for Hono):

```ts
// app/api/comms-id/[product]/[operation]/route.ts (Next.js). One route serves every product
// your pages use; the relay forwards only the operations these descriptors list.
import { addressProduct } from "@comms-id/address/descriptor";
import { asxProduct } from "@comms-id/asx/descriptor";
import { charityProduct } from "@comms-id/charity/descriptor";
import { logoProduct } from "@comms-id/logo/descriptor";
import { createNextRelay } from "@comms-id/relay/next";
import { sanctionsProduct } from "@comms-id/sanctions/descriptor";
import { createSigner } from "./signer";

export const { POST } = createNextRelay({
  products: [addressProduct, asxProduct, charityProduct, logoProduct, sanctionsProduct],
  // Your rule: who may use these routes. Return true, false (401) or your own Response.
  allow: (request) => request.headers.get("cookie") !== null,
  signer: createSigner(),
  // TEST: a development app's key and the TEST service. Going live: delete this line.
  baseUrl: "https://api-test.comms.id",
});
```

The signer is your own (the relay takes any function that returns a two-minute token for an audience and scope). This one reads your key from the environment and uses WebCrypto only, so it runs in Node 20+, Workers, Deno and Bun:

```ts
// The signer your relay needs: a two-minute EdDSA (Ed25519) token for one product, signed with
// your app's private key. The key is read once from the environment and never leaves this
// module. Runs in Node 20+, Workers, Deno and Bun (WebCrypto only).
import type { RelaySigner } from "@comms-id/relay";

const PADDING = /=+$/;
const encode = (bytes: ArrayBuffer | Uint8Array): string =>
  btoa(String.fromCharCode(...new Uint8Array(bytes)))
    .replaceAll("+", "-")
    .replaceAll("/", "_")
    .replace(PADDING, "");
const json = (value: unknown): string => encode(new TextEncoder().encode(JSON.stringify(value)));

/** A signer for `@comms-id/relay` from COMMS_ID_CLIENT_ID and COMMS_ID_PRIVATE_JWK. */
export const createSigner = (env: Record<string, string | undefined> = process.env): RelaySigner => {
  const clientId = env.COMMS_ID_CLIENT_ID?.trim();
  const jwk = env.COMMS_ID_PRIVATE_JWK;
  if (!(clientId && jwk)) {
    return () => undefined; // the relay then refuses with NOT_CONFIGURED; nothing is forwarded
  }
  const key = crypto.subtle.importKey("jwk", JSON.parse(jwk), { name: "Ed25519" }, false, ["sign"]);
  return async ({ audience, scope }) => {
    const now = Math.floor(Date.now() / 1000);
    const claims = {
      iss: clientId,
      sub: crypto.randomUUID(),
      jti: crypto.randomUUID(),
      aud: audience,
      scope,
      iat: now,
      exp: now + 120,
    };
    const input = `${json({ alg: "EdDSA" })}.${json(claims)}`;
    const signature = await crypto.subtle.sign("Ed25519", await key, new TextEncoder().encode(input));
    return `${input}.${encode(signature)}`;
  };
};
```

TEST first: the route above names `baseUrl: "https://api-test.comms.id"` and your development key answers there. LIVE: delete that line and deploy with the production key. The code between does not change.

## TEST first

With the relay pointed at TEST, type `50169561394` (the published example) in the form: the field shows a found charity and the hidden fields fill. `10000000000` is TEST's documented "no charity", `10000000032` its outage.

In your component tests, pass `createCharityFixtureClient()` from `@comms-id/charity/fixtures` as `client`: the form renders and answers with no network.

## LIVE

Remove `baseUrl` from the relay route and deploy with the production key. Reference: [Charity API](/docs/reference/charity). The attribution the field shows (ACNC, via data.gov.au, CC BY 3.0 AU) is part of the licence: do not hide it.
