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
61 changes: 61 additions & 0 deletions apps/web/app/__tests__/operated-by.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Carrier reviewers (Twilio toll-free verification, rejection 30482) match the
* contact email's domain to the operator, so every public policy page and the
* site footer name Noctusoft, Inc. as the operator.
*/
import { render, screen } from '@testing-library/react';
import type { AnchorHTMLAttributes, ReactNode } from 'react';
import { describe, expect, it, vi } from 'vitest';

import { PublicLegalFooter } from '@/components/PublicLegalFooter';

import HomePage from '../page';
import PrivacyPage from '../privacy/page';
import SmsOptInPage from '../sms/page';
import TermsPage from '../terms/page';

vi.mock('next/link', () => ({
default: ({ children, href, ...rest }: AnchorHTMLAttributes<HTMLAnchorElement> & { children: ReactNode }) => (
<a href={href} {...rest}>
{children}
</a>
),
}));

vi.mock('@/components/SmsOptInPageForm', () => ({
SmsOptInPageForm: () => <form data-testid="form-sms-opt-in" />,
}));

const OPERATED_BY = 'FieldView.Live is operated by Noctusoft, Inc. (https://noctusoft.com)';

function expectOperatedBy(container: HTMLElement): void {
const line = container.querySelector('[data-testid="text-operated-by"]');
expect(line).not.toBeNull();
expect(line).toHaveTextContent(OPERATED_BY);
expect(line?.querySelector('a')).toHaveAttribute('href', 'https://noctusoft.com');
}

describe('operated by Noctusoft, Inc.', () => {
it('is in the site footer', () => {
render(<PublicLegalFooter />);
expectOperatedBy(screen.getByTestId('footer-public-legal'));
});

it('is in the home page business section', () => {
render(<HomePage />);
expectOperatedBy(screen.getByTestId('section-about-business'));
});

it.each([
['privacy', PrivacyPage, 'page-privacy'],
['terms', TermsPage, 'page-terms'],
['sms', SmsOptInPage, 'page-sms'],
])('is near the top of /%s', (_name, Page, testId) => {
render(<Page />);
const page = screen.getByTestId(testId);
expectOperatedBy(page);
// Right under the page heading, before the body copy.
const heading = page.querySelector('h1');
expect(heading?.nextElementSibling).toHaveAttribute('data-testid', 'text-operated-by');
});
});
5 changes: 4 additions & 1 deletion apps/web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Link from 'next/link';

import { OperatedBy } from '@/components/OperatedBy';

export default function HomePage() {
return (
<div className="min-h-screen flex flex-col" data-testid="container-home">
Expand Down Expand Up @@ -90,8 +92,9 @@ export default function HomePage() {
</h2>
<p>
FieldView.Live streams youth sports games. Teams and event owners share one link; parents and fans watch
free or buy a game. FieldView.Live is operated by Noctusoft, Inc.
free or buy a game.
</p>
<OperatedBy />
<p>
Support:{' '}
<a
Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SMS_BRAND, SMS_SUPPORT_EMAIL } from '@fieldview/data-model';
import type { Metadata } from 'next';

import { OperatedBy } from '@/components/OperatedBy';

export const metadata: Metadata = {
title: 'Privacy Policy',
description: `Privacy policy for ${SMS_BRAND}`,
Expand All @@ -10,6 +12,7 @@ export default function PrivacyPage() {
return (
<main className="mx-auto max-w-3xl px-4 py-10 prose prose-neutral dark:prose-invert" data-testid="page-privacy">
<h1>Privacy Policy</h1>
<OperatedBy />
<p>Last updated: October 1, 2026</p>
<p>
{SMS_BRAND} (&quot;we&quot;, &quot;us&quot;) operates the FieldView.Live streaming platform. This policy describes how we
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/sms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SMS_BRAND, SMS_SUPPORT_EMAIL } from '@fieldview/data-model';
import type { Metadata } from 'next';
import Link from 'next/link';

import { OperatedBy } from '@/components/OperatedBy';
import { SmsOptInPageForm } from '@/components/SmsOptInPageForm';

export const metadata: Metadata = {
Expand All @@ -13,6 +14,7 @@ export default function SmsOptInPage() {
return (
<main className="mx-auto max-w-2xl px-4 py-10" data-testid="page-sms">
<h1 className="text-3xl font-bold">Text messages from {SMS_BRAND}</h1>
<OperatedBy className="mt-2 text-sm text-muted-foreground" />
<p className="mt-4 text-muted-foreground">
{SMS_BRAND} streams youth sports games. Parents and fans can choose to get game alerts, score updates, and
payment links by text for the teams and games they follow or buy. These are messages about games you chose,
Expand Down
3 changes: 3 additions & 0 deletions apps/web/app/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SMS_BRAND, SMS_PURPOSE_PHRASE, SMS_SUPPORT_EMAIL } from '@fieldview/data-model';
import type { Metadata } from 'next';

import { OperatedBy } from '@/components/OperatedBy';

export const metadata: Metadata = {
title: 'Terms of Service',
description: `Terms of service for ${SMS_BRAND}`,
Expand All @@ -10,6 +12,7 @@ export default function TermsPage() {
return (
<main className="mx-auto max-w-3xl px-4 py-10 prose prose-neutral dark:prose-invert" data-testid="page-terms">
<h1>Terms of Service</h1>
<OperatedBy />
<p>Last updated: October 1, 2026</p>

<section id="sms" data-testid="section-terms-sms">
Expand Down
23 changes: 23 additions & 0 deletions apps/web/components/OperatedBy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const OPERATOR_NAME = 'Noctusoft, Inc.';
const OPERATOR_URL = 'https://noctusoft.com';

export interface OperatedByProps {
className?: string;
}

/**
* Names the company behind FieldView.Live. Carrier reviewers (Twilio
* toll-free verification) match the contact email's domain to this operator,
* so it appears in the site footer and at the top of each policy page.
*/
export function OperatedBy({ className }: OperatedByProps) {
return (
<p className={className} data-testid="text-operated-by">
FieldView.Live is operated by {OPERATOR_NAME} (
<a href={OPERATOR_URL} className="underline underline-offset-4 hover:text-foreground" data-testid="link-operator">
{OPERATOR_URL}
</a>
)
</p>
);
}
3 changes: 3 additions & 0 deletions apps/web/components/PublicLegalFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Link from 'next/link';

import { OperatedBy } from '@/components/OperatedBy';

export function PublicLegalFooter() {
return (
<footer
Expand All @@ -20,6 +22,7 @@ export function PublicLegalFooter() {
</Link>
</nav>
</div>
<OperatedBy className="mx-auto mt-2 max-w-6xl" />
</footer>
);
}
Loading