Skip to content
Merged
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
2 changes: 1 addition & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pnpm --filter crowdpm-frontend preview

## Testing

The frontend uses Playwright for a small smoke/regression suite. The tests cover public routing, protected-route gating, mocked dashboard data, admin access, sign-out, and the node checkout redirect contract.
The frontend uses Playwright for a small smoke/regression suite. The tests cover public routing, protected-route gating, mocked dashboard data, admin access, sign-out, and the node waitlist submission contract.

Run from the repository root:

Expand Down
13 changes: 8 additions & 5 deletions frontend/e2e/app-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ test("admin route is role-gated and renders admin workflows for super admins", a
await expect(adminPage.getByText("admin.e2e@example.com")).toBeVisible();
});

test("node checkout flow uses the API redirect contract", async ({ page }) => {
test("node waitlist flow records a non-binding pledge", async ({ page }) => {
await page.goto("/node");
await expect(page.getByRole("heading", { name: "Products - CrowdPM Node Hardware" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Join the CrowdPM node waitlist before paid reservations open." })).toBeVisible();

await page.getByRole("button", { name: "Checkout - $375.00" }).click();
await expect(page).toHaveURL(/\/node\?checkout=success$/);
await expect(page.getByText("Checkout completed. Stripe will email a receipt.")).toBeVisible();
await expect(page.getByRole("button", { name: "Paid Reservations Paused" })).toBeDisabled();
await page.getByPlaceholder("Name").fill("Expo Visitor");
await page.getByPlaceholder("Email").fill("visitor@example.com");
await page.getByRole("checkbox").check();
await page.getByRole("button", { name: "Join Reservation Waitlist" }).click();
await expect(page.getByText("You are on the reservation waitlist.")).toBeVisible();
});
8 changes: 8 additions & 0 deletions frontend/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ export async function mockCrowdPmApi(page: Page) {
if (method === "GET" && path === "/v1/admin/submissions") return json(route, { submissions: batches });
if (method === "GET" && path === "/v1/admin/users") return json(route, adminUsers);
if (method === "GET" && path === "/v1/admin/demo-batch") return json(route, { deviceId: "device-e2e-1", batchId: "batch-e2e-1", summary: batches[0] });
if (method === "POST" && path === "/v1/node-reservation-pledges") {
return json(route, {
pledgeId: "pledge_e2e_node",
status: "recorded",
created: true,
intendedQuantity: 1,
});
}
if (method === "POST" && path === "/v1/node-purchase/checkout-session") {
return json(route, {
sessionId: "cs_e2e_node",
Expand Down
28 changes: 19 additions & 9 deletions frontend/src/components/LegalDocumentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,20 @@ function TermsOfService() {
</P>
</Section>

<Section title="3. Paid Products, Reservations, and Support">
<Section title="3. Waitlists, Paid Products, Reservations, and Support">
<P>
CrowdPM founding node reservations and certification support contributions are processed
through Stripe Checkout for {COMPANY_NAME}. A founding node reservation is a conditional
preorder for planned physical CrowdPM node hardware. It is not immediate delivery and no
node will be shipped, delivered, transferred, or released to an end user before required
FCC equipment authorization is complete.
CrowdPM may collect non-binding node reservation waitlist submissions before paid
reservations are available. A waitlist submission is not a preorder, purchase,
reservation, deposit, or guarantee that hardware will be available. No payment is collected
for joining the waitlist. If paid reservations open later, CrowdPM may email waitlist
contacts with release details.
</P>
<P>
When enabled, CrowdPM founding node reservations and certification support contributions
are processed through Stripe Checkout for {COMPANY_NAME}. A founding node reservation is
a conditional preorder for planned physical CrowdPM node hardware. It is not immediate
delivery and no node will be shipped, delivered, transferred, or released to an end user
before required FCC equipment authorization is complete.
</P>
<P>
The founding node reservation price is $375 per reserved node with standard US shipping
Expand All @@ -178,9 +185,9 @@ function TermsOfService() {
fulfillment issues make shipment impractical.
</P>
<P>
Certification support is a support-only contribution toward FCC testing and launch costs.
It does not reserve hardware, include shipment, create equity or debt, grant a service
entitlement, or qualify as a charitable tax-deductible donation.
When enabled, certification support is a support-only contribution toward FCC testing and
launch costs. It does not reserve hardware, include shipment, create equity or debt, grant
a service entitlement, or qualify as a charitable tax-deductible donation.
</P>
<P>
CrowdPM may also offer one-time digital expansion purchases processed through Stripe
Expand Down Expand Up @@ -332,6 +339,7 @@ function PrivacyPolicy() {
<Bullet>User settings, including default batch visibility, map/rendering preferences, and theme preferences.</Bullet>
<Bullet>Device records, including device IDs, optional device names, owner IDs, model/version, status, fingerprints, public key material, pairing codes, token records, and last-seen timestamps.</Bullet>
<Bullet>Measurement and batch data, including PM2.5 readings, pollutant/unit, latitude, longitude, altitude, precision, timestamp, flags, batch IDs, visibility, moderation state, storage paths, and related metadata.</Bullet>
<Bullet>Node reservation waitlist records, including name, email address, intended quantity, consent status, submission source, submission timestamps, and deduplication metadata.</Bullet>
<Bullet>Paid product records, including Stripe Checkout session IDs, payment status, campaign tier, reservation or support quantity, customer contact details, billing and shipping addresses where applicable, order totals, tax amounts, shipping details for hardware reservations, receipts, refunds, support messages, and related fulfillment or entitlement records.</Bullet>
<Bullet>Moderation and administration records, including moderator user IDs, role changes, disabled account status, moderation reasons, and audit records.</Bullet>
<Bullet>Technical and security data, including IP address or network hints, request headers, logs, rate-limit keys, browser/device information available to the service, and error diagnostics.</Bullet>
Expand All @@ -350,6 +358,7 @@ function PrivacyPolicy() {
<BulletList>
<Bullet>Provide authentication, device pairing, ingest, map display, dashboards, moderation, and administration features.</Bullet>
<Bullet>Store and process sensor measurements, enforce visibility choices, and show public batches on the map and public API.</Bullet>
<Bullet>Maintain node reservation waitlists, estimate first-run demand, deduplicate submissions, and email waitlist contacts when paid reservations are ready to open.</Bullet>
<Bullet>Process paid product purchases, node reservations, certification support contributions, tax calculation, authorized hardware shipments, digital entitlements, receipts, refunds or replacements, and order support requests.</Bullet>
<Bullet>Protect the service through rate limiting, abuse detection, access controls, logging, audits, token revocation, and troubleshooting.</Bullet>
<Bullet>Save preferences and improve reliability, usability, and performance.</Bullet>
Expand Down Expand Up @@ -380,6 +389,7 @@ function PrivacyPolicy() {
<BulletList>
<Bullet>With Google Firebase and Google Cloud services used for authentication, hosting, functions, Firestore, storage, and operational logs.</Bullet>
<Bullet>With Stripe for payment processing, sales tax calculation, checkout, receipts, fraud prevention, refunds, and related payment operations.</Bullet>
<Bullet>With email or customer-communication providers if needed to send reservation availability or waitlist-related messages.</Bullet>
<Bullet>With shipping, fulfillment, repair, or customer-support providers where needed to deliver or support authorized node hardware reservations.</Bullet>
<Bullet>With the public, when you or a device under your account submits public approved batch data.</Bullet>
<Bullet>With authorized moderators and administrators who need access to operate, secure, moderate, or support CrowdPM.</Bullet>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {
NodeCampaignTierId,
NodePurchaseReceipt,
NodePurchaseVariantId,
NodeReservationPledgeRequest,
NodeReservationPledgeResponse,
PublicBatchDetail,
PublicBatchMapResponse,
PublicBatchSummary,
Expand Down Expand Up @@ -125,6 +127,8 @@ export type {
NodeCampaignTierId,
NodePurchaseReceipt,
NodePurchaseVariantId,
NodeReservationPledgeRequest,
NodeReservationPledgeResponse,
PublicBatchDetail,
PublicBatchMapResponse,
PublicBatchSummary,
Expand Down Expand Up @@ -172,6 +176,18 @@ export async function listNodePurchaseReceipts(): Promise<NodePurchaseReceipt[]>
return requestJson<NodePurchaseReceipt[]>("/v1/node-purchase/receipts");
}

export async function submitNodeReservationPledge(
payload: NodeReservationPledgeRequest,
): Promise<NodeReservationPledgeResponse> {
return requestJson<NodeReservationPledgeResponse>("/v1/node-reservation-pledges", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
}

export async function createThemeSaveCheckoutSession(): Promise<CheckoutRedirectSession> {
return requestJson<CheckoutRedirectSession>("/v1/theme-purchase/checkout-session", {
method: "POST",
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const HIGHLIGHTS = [
description: "Browse shared PM2.5 measurements from the dedicated live map experience.",
},
{
title: "Conditional node launch",
description: "Reserve first-run hardware or support FCC testing without implying immediate shipment.",
title: "Node launch waitlist",
description: "Join the first-run interest list while paid reservations and certification support payments are paused.",
},
{
title: "Batch exports",
Expand Down Expand Up @@ -85,8 +85,8 @@ export default function HomePage({
</Heading>
<Text size="3" color="gray" as="p" style={{ maxWidth: 680 }}>
CrowdPM combines open hardware, account-scoped activation, public measurement publishing,
and a 3D map for community air quality data. Expo payments are structured as certification
support or conditional reservations that ship only after FCC authorization.
and a 3D map for community air quality data. Expo interest is collected through a
non-binding waitlist before paid reservations open.
</Text>
</Flex>

Expand Down Expand Up @@ -117,7 +117,7 @@ export default function HomePage({

<Flex gap="3" wrap="wrap">
<Button variant="ghost" onClick={onOpenProducts}>
Reserve or support
Join node waitlist
</Button>
<Button variant="ghost" onClick={onOpenAbout}>
Learn about the project
Expand Down
Loading
Loading