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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ All notable changes are documented here. Format based on [Keep a Changelog](http
and export controls (#555)
- `/stream/[id]` — **PDF** button triggers `window.print()` to produce a formatted single-stream
summary document via the print stylesheet; actions and back-link hidden in print view (#571)
- `/stream/[id]` — **Calendar** button downloads a standard `.ics` invitation for the stream's
end date (one-hour event, one-hour reminder, deterministic UID so re-importing updates the
existing entry instead of duplicating it), built by the new `lib/calendar.ts`; rendered only
for bounded streams, since an open-ended stream has no end date to schedule (#566)
- `app/globals.css` — `@media print` block scoped to `.print-receipt` class: resets body
to white/black, collapses table borders, hides interactive chrome, preserves monospace
addresses and amounts, renders status badges in monochrome, adds a print footer with the
Expand Down
1 change: 1 addition & 0 deletions __mocks__/lucide-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const AlertCircle = createIconStub('AlertCircle');
export const ArrowDownToLine = createIconStub('ArrowDownToLine');
export const ArrowLeft = createIconStub('ArrowLeft');
export const ArrowRight = createIconStub('ArrowRight');
export const CalendarPlus = createIconStub('CalendarPlus');
export const Check = createIconStub('Check');
export const CheckCircle = createIconStub('CheckCircle');
export const Clock = createIconStub('Clock');
Expand Down
50 changes: 50 additions & 0 deletions app/stream/[id]/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ vi.mock('@/components/stream/StreamActions', () => ({
StreamActions: ({ token }: { token: string }) =>
React.createElement('div', { 'data-testid': 'stream-actions' }, token),
}));
vi.mock('@/components/stream/AddToCalendarButton', () => ({
AddToCalendarButton: ({ streamId, endTime }: { streamId: string; endTime: number }) =>
React.createElement('div', {
'data-testid': 'add-to-calendar',
'data-stream-id': streamId,
'data-end-time': String(endTime),
}),
}));

import { useWallet } from '@/contexts/WalletContext';
import { getStreamAddress, getStreamInfo, getWithdrawable } from '@/lib/stream';
Expand Down Expand Up @@ -269,4 +277,46 @@ describe('StreamPage (app/stream/[id]/page.tsx)', () => {
// silently stale "active" state from the first request.
expect(container.querySelector('[data-testid="badge"]')?.textContent).toBe('cancelled');
});

it('#566 — offers the calendar action with the stream end date when the stream is bounded', async () => {
mockUseWallet.mockReturnValue({
publicKey: 'GRECIPIENT',
connected: true,
} as unknown as ReturnType<typeof useWallet>);
mockAddr.mockResolvedValue('STREAM_ADDR');
const endTime = Math.floor(Date.now() / 1000) + 3600;
mockInfo.mockResolvedValue(makeInfo({ endTime }));
mockWithdrawable.mockResolvedValue(0n);

await act(async () => {
root.render(React.createElement(StreamPage));
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});

const action = container.querySelector('[data-testid="add-to-calendar"]');
expect(action).not.toBeNull();
expect(action?.getAttribute('data-stream-id')).toBe('42');
expect(action?.getAttribute('data-end-time')).toBe(String(endTime));
});

it('#566 — omits the calendar action for an open-ended stream (nothing to schedule)', async () => {
mockUseWallet.mockReturnValue({
publicKey: 'GRECIPIENT',
connected: true,
} as unknown as ReturnType<typeof useWallet>);
mockAddr.mockResolvedValue('STREAM_ADDR');
mockInfo.mockResolvedValue(makeInfo({ endTime: 0 }));
mockWithdrawable.mockResolvedValue(0n);

await act(async () => {
root.render(React.createElement(StreamPage));
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});

expect(container.querySelector('[data-testid="add-to-calendar"]')).toBeNull();
});
});
12 changes: 12 additions & 0 deletions app/stream/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { StreamActions } from "@/components/stream/StreamActions";
import { AddToCalendarButton } from "@/components/stream/AddToCalendarButton";
import { OperatorInfo } from "@/components/stream/OperatorInfo";
import { StreamNoteEditor } from "@/components/stream/StreamNoteEditor";
import { AddToCalendarButton } from "@/components/stream/AddToCalendarButton";
import { useWallet } from "@/contexts/WalletContext";
import { useStreamNote } from "@/hooks/useStreamNote";
import {
Expand Down Expand Up @@ -324,6 +325,17 @@ export default function StreamPage() {
<FileText className="w-3.5 h-3.5" aria-hidden="true" />
PDF
</button>
{/* Add the stream's end date to an external calendar (#566) — only
bounded streams have an end date to schedule. */}
{info.endTime > 0 && (
<AddToCalendarButton
streamId={id}
endTime={info.endTime}
startTime={info.startTime}
streamAddress={streamAddress}
tokenSymbol={tokenSymbol}
/>
)}
</div>
</div>

Expand Down