Rebrand: Co-op Check-in -> Co-op Survey in user-facing copy

Updates the tool product name across the app UI (header, page title,
section labels, form buttons, success/error states, report stat labels),
README, deployment docs, and the CiviCRM email template guidance.
Custom domain references move from check-in.fci.coop to survey.fci.coop
(DNS update still required).

The underlying CiviCRM "Check-in (organizing)" activity type, custom
group machine names (Check_in_data__organizing_), health-check ids,
and the internal org_engagement_check_in form id are unchanged --
those are CiviCRM contract surfaces, not product copy.
This commit is contained in:
Joel Brock
2026-06-04 17:12:03 -07:00
parent 9bc4bbb732
commit b120075ca2
14 changed files with 53 additions and 53 deletions
+1 -1
View File
@@ -98,7 +98,7 @@ useful to confirm env vars are wired correctly.
## Step 4 — Custom domain (optional)
Amplify can attach a custom domain (e.g. `check-in.fci.coop`) with
Amplify can attach a custom domain (e.g. `survey.fci.coop`) with
auto-issued TLS in a few minutes:
1. In the app, **Hosting****Custom domains****Add domain**.
+2 -2
View File
@@ -28,7 +28,7 @@ Verify all of the above against your live CiviCRM by hitting `/api/health` (in d
| `CIVI_HTTP_AUTH_USER` | only if Civi sits behind webserver-level Basic Auth | HTTP Basic Auth username. |
| `CIVI_HTTP_AUTH_PASS` | only if CIVI_HTTP_AUTH_USER is set | HTTP Basic Auth password. |
| `HEALTH_TOKEN` | recommended | Long random string (e.g. `openssl rand -hex 32`). Required to access `/api/health` in production. If unset, that route returns 404. |
| `PUBLIC_ORIGIN` | optional | e.g. `https://check-in.fci.coop` — used in absolute self-links if needed later. |
| `PUBLIC_ORIGIN` | optional | e.g. `https://survey.fci.coop` — used in absolute self-links if needed later. |
3. **Trigger the first deploy.** Render will run `npm ci && npm run build` then `npm run start`. The platform health check hits `/healthz` (lightweight, no Civi dependency).
@@ -37,7 +37,7 @@ Verify all of the above against your live CiviCRM by hitting `/api/health` (in d
- `https://<your-render-url>/api/health?token=<HEALTH_TOKEN>` → all checks should be green
- Visit `https://<your-render-url>/?cid=<test-individual>&cs=<their-checksum>` → form loads with prefill
5. **Custom domain (optional)** — Render dashboard → Custom Domain → add `check-in.fci.coop`. Update DNS to the provided CNAME. Render auto-issues a Let's Encrypt cert.
5. **Custom domain (optional)** — Render dashboard → Custom Domain → add `survey.fci.coop`. Update DNS to the provided CNAME. Render auto-issues a Let's Encrypt cert.
## Security defaults
+15 -15
View File
@@ -1,10 +1,10 @@
# Email link delivery
The check-in form is reached via a personalized URL that carries the
The survey form is reached via a personalized URL that carries the
contact's CiviCRM ID + a server-issued checksum:
```
https://check-in.fci.coop/?cid=<contact_id>&cs=<checksum>
https://survey.fci.coop/?cid=<contact_id>&cs=<checksum>
```
This document covers the three pieces needed to actually get those links
@@ -24,10 +24,10 @@ The preview endpoint (#3) is a debugging convenience.
In CiviCRM:
1. Navigate to **Mailings → Message Templates → Add Message Template**.
2. Title: `Co-op Check-in invitation`
2. Title: `Co-op Survey invitation`
3. Subject:
```
Time for your co-op check-in
Time for your co-op survey
```
4. Plain-text body (substitute your real domain):
@@ -38,8 +38,8 @@ In CiviCRM:
month. The form pre-fills your prior responses — you only need to update
what's changed.
Open your check-in:
https://check-in.fci.coop/?cid={contact.contact_id}&cs={contact.checksum}
Open your survey:
https://survey.fci.coop/?cid={contact.contact_id}&cs={contact.checksum}
The link is personalized to you and expires in 14 days. If you no longer
have it, reply to this email and we'll send a fresh one.
@@ -58,11 +58,11 @@ In CiviCRM:
what's changed.</p>
<p style="margin: 24px 0;">
<a href="https://check-in.fci.coop/?cid={contact.contact_id}&amp;cs={contact.checksum}"
<a href="https://survey.fci.coop/?cid={contact.contact_id}&amp;cs={contact.checksum}"
style="display: inline-block; background: #3a5520; color: #fafaf7;
padding: 10px 20px; border-radius: 6px; text-decoration: none;
font-weight: 500;">
Open your check-in
Open your survey
</a>
</p>
@@ -95,14 +95,14 @@ Best for: low volume, one-at-a-time, or you want to review each send.
1. Open the **organization's contact record** in CiviCRM.
2. **Relationships** tab → find the row with **"Primary Contact"** → click the related individual's name.
3. On the individual's contact view: **Actions → Send Email**.
4. **Use Template**: select `Co-op Check-in invitation`. The subject and body populate; tokens render in the preview.
4. **Use Template**: select `Co-op Survey invitation`. The subject and body populate; tokens render in the preview.
5. **Send**. CiviCRM logs the email as an Activity on the contact.
Repeat per contact. Slow but bulletproof.
### Option B — Bulk: CiviMail (campaign-style send)
Best for: send the check-in invitation to every active co-op at once (e.g. monthly).
Best for: send the survey invitation to every active co-op at once (e.g. monthly).
1. Build a Smart Group of contacts whose Primary Contact relationships point at orgs in active stages. Example query:
- **Contacts** → **Advanced Search**
@@ -112,7 +112,7 @@ Best for: send the check-in invitation to every active co-op at once (e.g. month
2. **Mailings → New Mailing**.
3. Recipients: the smart group above.
4. Choose template: `Co-op Check-in invitation`.
4. Choose template: `Co-op Survey invitation`.
5. Schedule send.
Each recipient gets their own checksum embedded in the URL — CiviMail
@@ -125,10 +125,10 @@ Best for: a button-on-the-org-record workflow.
Setup:
1. **Administer → CiviRules → Manage Rules → New Rule**.
2. Title: `Send Co-op Check-in invitation`.
2. Title: `Send Co-op Survey invitation`.
3. **Trigger**: pick a "manually trigger from org row" action if your CiviRules version supports it. Otherwise: trigger on org Stage change (auto-fires when staff advance an org).
4. **Action**: `Send email to contact via message template`.
- Template: `Co-op Check-in invitation`.
- Template: `Co-op Survey invitation`.
- Recipient: **Contact in relationship** → "Primary Contact" → side B (the Individual).
5. Save.
@@ -153,7 +153,7 @@ contact/org context for verification.
**Request:**
```bash
curl "https://check-in.fci.coop/api/preview-link?cid=513&token=$HEALTH_TOKEN"
curl "https://survey.fci.coop/api/preview-link?cid=513&token=$HEALTH_TOKEN"
```
**Response:**
@@ -165,7 +165,7 @@ curl "https://check-in.fci.coop/api/preview-link?cid=513&token=$HEALTH_TOKEN"
"orgId": 9609,
"orgName": "A Sample Food Co-op",
"orgStage": "Organizing",
"url": "https://check-in.fci.coop/?cid=513&cs=8d1f...",
"url": "https://survey.fci.coop/?cid=513&cs=8d1f...",
"checksum": "8d1f...",
"ttlHours": 336
}
+5 -5
View File
@@ -1,4 +1,4 @@
# Co-op Check-in
# Co-op Survey
A tokenized, mobile-friendly web form that lets external co-op contacts
update their organization's tracking data on CiviCRM, plus a read-only
@@ -11,8 +11,8 @@ Each co-op has a designated **Primary Contact** (the individual) linked
to the **Organization** record in CiviCRM. Staff send that contact a
personalized link generated against the contact's CiviCRM checksum:
- `https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>` — the form
- `https://check-in.fci.coop/report?cid=<contactId>&cs=<checksum>` — the report
- `https://survey.fci.coop/?cid=<contactId>&cs=<checksum>` — the form
- `https://survey.fci.coop/report?cid=<contactId>&cs=<checksum>` — the report
When the link is opened, the app verifies the checksum against CiviCRM,
resolves the organization through the Primary Contact relationship, and
@@ -24,12 +24,12 @@ Build → Fulfill & Stabilize). The org's current stage controls which
sections are editable; past and current stages are open for editing,
future stages render as previews with their fields locked so the co-op
can see the framework ahead. Fields prefill with each value's most
recent non-empty entry from past check-ins. On submit, a new
recent non-empty entry from past surveys. On submit, a new
"Check-in (organizing)" activity is created; nothing is overwritten.
**Stage authority.** The current stage is derived from the most recent
"Check-in (organizing)" activity whose Stage field is set. Staff own
stage transitions by manually setting Stage on a check-in activity they
stage transitions by manually setting Stage on a "Check-in (organizing)" activity they
create; the form itself never writes Stage, so org self-submissions
can't override a staff transition. Orgs with no stage-bearing activity
default to "Inquiry."
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* GET /api/preview-link?cid=<contactId>&token=<HEALTH_TOKEN>
*
* Returns a ready-to-send check-in URL for a given contact, plus enough
* Returns a ready-to-send survey URL for a given contact, plus enough
* context for staff to verify it's the right org. Used for:
* - Generating links to paste into ad-hoc emails (or external tools)
* - Testing a contact's setup before sending the real CiviCRM email
+4 -4
View File
@@ -49,11 +49,11 @@ const STUB_PAYLOAD: ReportPayload = (() => {
orgName: "Sample Co-op (stub)",
currentStage: "Organizing",
activities: [
{ id: 9012, date: daysAgo(3), subject: "Co-op Check-in (form submission)" },
{ id: 9008, date: daysAgo(34), subject: "Co-op Check-in (form submission)" },
{ id: 9012, date: daysAgo(3), subject: "Co-op Survey (form submission)" },
{ id: 9008, date: daysAgo(34), subject: "Co-op Survey (form submission)" },
{ id: 9001, date: daysAgo(62), stage: "Organizing", subject: "Stage transition (staff)" },
{ id: 8995, date: daysAgo(95), subject: "Co-op Check-in (form submission)" },
{ id: 8980, date: daysAgo(180), stage: "Inquiry", subject: "Initial check-in (staff)" },
{ id: 8995, date: daysAgo(95), subject: "Co-op Survey (form submission)" },
{ id: 8980, date: daysAgo(180), stage: "Inquiry", subject: "Initial survey (staff)" },
],
fieldHistory: {
Peer_Group_Participation: [
+2 -2
View File
@@ -70,7 +70,7 @@ export async function POST(req: Request) {
return NextResponse.json(
{
error: appEnv().isProduction
? "Could not save your check-in. Please try again, or contact your engagement coordinator."
? "Could not save your survey. Please try again, or contact your engagement coordinator."
: `Save failed: ${redact(msg)}`,
},
{ status: 500 },
@@ -110,7 +110,7 @@ async function runSubmit(cid: string, cs: string, values: Record<string, unknown
"status_id:name": "Completed",
target_contact_id: orgId,
source_contact_id: Number(cid),
subject: "Co-op Check-in (form submission)",
subject: "Co-op Survey (form submission)",
};
for (const [name, value] of Object.entries(values)) {
const field = FIELD_BY_NAME.get(name);
+1 -1
View File
@@ -28,7 +28,7 @@ const body = Open_Sans({
});
export const metadata: Metadata = {
title: "Co-op Check-in · Food Co-op Initiative",
title: "Co-op Survey · Food Co-op Initiative",
description:
"Update your co-op's progress through the FCI organizing Framework. A survey for food co-ops in development.",
robots: { index: false, follow: false }, // Form pages are tokenized; not for crawlers.
+1 -1
View File
@@ -52,7 +52,7 @@ function ReportIntro() {
</h1>
<p className="mt-4 max-w-prose text-[17px] leading-relaxed text-ink-soft">
A read-only summary of every value your co-op has shared through past
check-ins, grouped by stage. The most recent entry sits at the top of
surveys, grouped by stage. The most recent entry sits at the top of
each row; expand a row to see how a number or note has changed over
time.
</p>
+9 -9
View File
@@ -429,7 +429,7 @@ function SubmissionContextHeader({
}) {
return (
<header className="rounded-lg border border-rule bg-paper-2/40 px-6 py-5 sm:px-7 sm:py-6">
<p className="text-[11px] uppercase tracking-[0.18em] text-ink-mute">Check-in for</p>
<p className="text-[11px] uppercase tracking-[0.18em] text-ink-mute">Survey for</p>
<h1 className="mt-1 font-display text-3xl font-medium leading-tight tracking-tight text-ink sm:text-[34px]">
{orgName}
</h1>
@@ -551,7 +551,7 @@ function SubmitBar({
} else if (state.kind === "submitting") {
secondary = (
<p className="truncate text-xs text-ink-soft" role="status">
Saving your check-in
Saving your survey
</p>
);
} else if (viewedSectionLabel && viewedRank != null) {
@@ -604,10 +604,10 @@ function SubmitBar({
>
{state.kind === "submitting" ? (
<>
<Spinner /> Saving check-in
<Spinner /> Saving survey
</>
) : (
<>Submit check-in</>
<>Submit survey</>
)}
</button>
</div>
@@ -636,7 +636,7 @@ function LoadingState() {
aria-hidden
className="mx-auto mb-4 h-7 w-7 animate-spin rounded-full border-[1.5px] border-rule border-t-leaf-700"
/>
<p className="font-display text-base text-ink-soft italic">Loading your check-in</p>
<p className="font-display text-base text-ink-soft italic">Loading your survey</p>
</div>
);
}
@@ -663,10 +663,10 @@ function SuccessDestination({
</svg>
</div>
<h2 className="mt-5 font-display text-2xl font-medium leading-tight tracking-tight text-ink sm:text-3xl">
Check-in saved
Survey saved
</h2>
<p className="mx-auto mt-3 max-w-prose text-[15px] leading-relaxed text-ink-soft">
Thank you. We&apos;ve recorded this check-in for{" "}
Thank you. We&apos;ve recorded this survey for{" "}
<span className="font-medium text-ink">{orgName}</span>. Your engagement coordinator will see
it on their next review.
</p>
@@ -676,7 +676,7 @@ function SuccessDestination({
onClick={onAnother}
className="inline-flex items-center justify-center rounded-md border border-rule bg-paper px-5 py-2 text-sm font-medium text-ink-soft transition hover:bg-paper-2/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-500/40"
>
Submit another check-in
Submit another survey
</button>
<p className="text-xs text-ink-mute">It&apos;s safe to close this window.</p>
</div>
@@ -691,7 +691,7 @@ function ErrorState({ message }: { message: string }) {
className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7"
>
<h2 className="font-display text-xl font-medium text-clay-700">
We couldn&apos;t open your check-in.
We couldn&apos;t open your survey.
</h2>
<p className="mt-3 text-sm leading-relaxed text-ink-soft">{message}</p>
<p className="mt-4 text-sm text-ink-soft">
+2 -2
View File
@@ -174,7 +174,7 @@ function ReportContextHeader({
{currentStageLabel || "—"}
</p>
<dl className="mt-5 grid grid-cols-3 gap-3 border-t border-rule-soft pt-4 text-sm sm:gap-6">
<Stat label="Check-ins" value={String(totalActivities)} />
<Stat label="Surveys" value={String(totalActivities)} />
<Stat label="Fields tracked" value={String(totalFieldsTracked)} />
<Stat
label="Span"
@@ -1414,7 +1414,7 @@ function EmptyState() {
<div className="rounded-lg border border-dashed border-rule bg-paper-2/30 px-6 py-10 text-center">
<p className="font-display text-lg text-ink-soft">No entries on file yet.</p>
<p className="mt-2 text-sm text-ink-mute">
Your co-op&apos;s first check-in will appear here once it&apos;s submitted.
Your co-op&apos;s first survey will appear here once it&apos;s submitted.
</p>
</div>
);
+1 -1
View File
@@ -19,7 +19,7 @@ export function SiteHeader() {
className="h-12 w-auto sm:h-14"
/>
<span className="hidden text-[11px] uppercase tracking-[0.18em] text-ink-mute sm:inline-block sm:border-l sm:border-rule sm:pl-4">
Co-op Check-in
Co-op Survey
</span>
</Link>
<nav aria-label="Primary">
+8 -8
View File
@@ -1,5 +1,5 @@
/**
* Form definition for the Org Engagement Check-in form.
* Form definition for the Org Engagement Survey form.
*
* Field references and types are sourced from the live CiviCRM DEV
* (`client.crm.fci.coop`) inventory pulled 2026-05-09 via APIv4
@@ -49,13 +49,13 @@ const G3 = "Stage_3";
const G4 = "Stage_4";
const G5 = "Stage_5";
// Stage 0 — Check-in (always visible)
// Stage 0 — Survey (always visible)
const stage0: StageSectionConfig = {
rank: 0,
id: "stage_0",
label: "Check-in (organizing)",
intro:
"Core check-in fields. These are visible at every stage and capture the data we follow over time across the lifecycle of the co-op.",
// intro:
// "Core survey fields. These are visible at every stage and capture the data we follow over time across the lifecycle of the co-op.",
fields: [
// current_stage is a UI-only readonly field. Its value is the Civi
// option *value* (e.g. "Organizing") sourced by /api/data from the
@@ -218,7 +218,7 @@ const stage0: StageSectionConfig = {
type: "currency",
visibleWhen: visibleAtOrAfter(S.Stabilize),
civiField: `${G0}.Member_equity_raised`,
help: "Member-owner equity raised, as of the time of check-in",
help: "Member-owner equity raised, as of the time of survey",
},
// {
// name: "Member_loans_total",
@@ -232,7 +232,7 @@ const stage0: StageSectionConfig = {
type: "currency",
visibleWhen: visibleAtOrAfter(S.Stabilize),
civiField: `${G0}.Member_loans_raised`,
help: "Total amount (USD) raised from member-owner loans, as of the time of check-in",
help: "Total amount (USD) raised from member-owner loans, as of the time of survey",
},
// {
// name: "Member_preferred_shares_total",
@@ -498,7 +498,7 @@ const stage3: StageSectionConfig = {
fieldGroups: [
{ id: "site", fields: ["Site_Letter_of_Intent_Date", "Site_Letter_of_Intent_Upload"] },
{ id: "store_design", fields: ["Store_Designer", "Store_Design_Plan_Completion"] },
{ id: "cap_camp", fields: ["Capital_Campaign_Owner_Participation_", "Capital_Campaign_Average_Owner_Investment"]},
// { id: "cap_camp", fields: ["Capital_Campaign_Owner_Participation_", "Capital_Campaign_Average_Owner_Investment"]},
],
};
@@ -713,7 +713,7 @@ const stage5: StageSectionConfig = {
export const formConfig: FormConfig = {
id: "org_engagement_check_in",
title: "Co-op Check-in",
title: "Co-op Survey",
subtitle:
"Update tracking data for your co-op as you progress through the organizing stages. The questions you'll see depend on where you are in the framework.",
stageField: "current_stage",
+1 -1
View File
@@ -203,7 +203,7 @@ export interface FormConfig {
/**
* Identifying details for the contact submitting the form, derived from the
* cid resolved by /api/data. Surfaced as readonly fields in Stage 0 so the
* person filling out the check-in can confirm who's logged as the submitter.
* person filling out the survey can confirm who's logged as the submitter.
*/
export interface ContactIdentity {
firstName: string;