diff --git a/CHANGELOG.md b/CHANGELOG.md index 66983576d8..652739da20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,28 +2,32 @@ --- -## 2026.01.23 +## 1/23/26 ### Payments Introduced a redesigned payments onboarding flow +![Payments Onboarding]() -## 2026.01.21 +## 1/21/26 ### Payments - Payments page updated with new UI changes +![Create Product]() - Added a new Payments Settings page with an option to temporarily disable all payments +![Payments Setting]() - Subscription renewal emails are now sent automatically to users - Past payment invoices are now visible on the Account Settings page +![Past Payments Invoices]() ### Documentation - Updated JWT documentation to include `isRestricted` and `restrictedReason` -## 2026.01.19 +## 1/19/26 - Updated package dependencies to their newest versions. -## 2025.12.19 +## 12/19/25 - Introduces new changelog and deprecates all older changelogs. -- Moved away from semantic versioning in favor of CalVer. +- Date versioning for public view. --- diff --git a/apps/backend/src/app/api/latest/internal/changelog/route.tsx b/apps/backend/src/app/api/latest/internal/changelog/route.tsx index e9768863ee..9fc35103d2 100644 --- a/apps/backend/src/app/api/latest/internal/changelog/route.tsx +++ b/apps/backend/src/app/api/latest/internal/changelog/route.tsx @@ -1,6 +1,8 @@ import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; +import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; +import * as fs from "fs/promises"; +import * as path from "path"; const REVALIDATE_SECONDS = 60 * 60; @@ -72,10 +74,9 @@ function parseRootChangelog(markdown: string): ChangelogEntry[] { const heading = versionMatch[1].trim(); const { version, releasedAt, isUnreleased } = parseVersionHeading(heading); - const isSemver = /^\d+\.\d+\.\d+$/.test(version); - const isCalVer = /^\d{4}\.\d{2}\.\d{2}$/.test(version); + const isUsDate = /^\d{1,2}\/\d{1,2}\/\d{2}$/.test(version); // US date format: M/D/YY - if (!isUnreleased && !isSemver && !isCalVer) { + if (!isUnreleased && !isUsDate) { continue; } @@ -138,6 +139,25 @@ export const GET = createSmartRouteHandler({ }).defined(), }), handler: async () => { + const isDevelopment = getNodeEnvironment() === "development"; + + // In development mode, read from local CHANGELOG.md file + if (isDevelopment) { + const changelogPath = path.resolve(process.cwd(), "../../CHANGELOG.md"); + const fileExists = await fs.access(changelogPath).then(() => true, () => false); + + if (fileExists) { + const content = await fs.readFile(changelogPath, "utf-8"); + const entries = parseRootChangelog(content).slice(0, 8); + + return { + statusCode: 200, + bodyType: "json", + body: { entries }, + } as const; + } + } + const changelogUrl = getEnvVariable("STACK_CHANGELOG_URL", ""); if (!changelogUrl) { diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index ca3959bb07..8aef410da0 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -64,6 +64,12 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'raw.githubusercontent.com', + port: '', + pathname: '/**', + }, ], }, diff --git a/apps/dashboard/public/changelog/account-settings-invoices.png b/apps/dashboard/public/changelog/account-settings-invoices.png new file mode 100644 index 0000000000..caf7c206e2 Binary files /dev/null and b/apps/dashboard/public/changelog/account-settings-invoices.png differ diff --git a/apps/dashboard/public/changelog/payments-create-product.png b/apps/dashboard/public/changelog/payments-create-product.png new file mode 100644 index 0000000000..9af12990aa Binary files /dev/null and b/apps/dashboard/public/changelog/payments-create-product.png differ diff --git a/apps/dashboard/public/changelog/payments-onboarding.png b/apps/dashboard/public/changelog/payments-onboarding.png new file mode 100644 index 0000000000..eb8ecc144f Binary files /dev/null and b/apps/dashboard/public/changelog/payments-onboarding.png differ diff --git a/apps/dashboard/public/changelog/payments-settings-1.png b/apps/dashboard/public/changelog/payments-settings-1.png new file mode 100644 index 0000000000..53f0f1afda Binary files /dev/null and b/apps/dashboard/public/changelog/payments-settings-1.png differ diff --git a/apps/dashboard/src/components/stack-companion.tsx b/apps/dashboard/src/components/stack-companion.tsx index d0ed8bd94a..c3b54c848d 100644 --- a/apps/dashboard/src/components/stack-companion.tsx +++ b/apps/dashboard/src/components/stack-companion.tsx @@ -15,19 +15,22 @@ import { FeatureRequestBoard } from './stack-companion/feature-request-board'; import { UnifiedDocsWidget } from './stack-companion/unified-docs-widget'; /** - * Compare two CalVer versions in YYYY.MM.DD format + * Compare two US date versions in M/D/YY format * Returns true if version1 is newer than version2 */ -function isNewerCalVer(version1: string, version2: string): boolean { - const parseCalVer = (version: string): Date | null => { - const match = version.match(/^(\d{4})\.(\d{2})\.(\d{2})$/); +function isNewerVersion(version1: string, version2: string): boolean { + const parseUsDate = (version: string): Date | null => { + const match = version.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2})$/); if (!match) return null; - const [, year, month, day] = match; - return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); + const [, month, day, year] = match; + const twoDigitYear = parseInt(year); + // Sliding window: 70-99 → 1970-1999, 00-69 → 2000-2069 + const fullYear = twoDigitYear >= 70 ? 1900 + twoDigitYear : 2000 + twoDigitYear; + return new Date(fullYear, parseInt(month) - 1, parseInt(day)); }; - const date1 = parseCalVer(version1); - const date2 = parseCalVer(version2); + const date1 = parseUsDate(version1); + const date2 = parseUsDate(version2); if (!date1 || !date2) { // Fallback to string comparison if parsing fails @@ -193,7 +196,7 @@ export function StackCompanion({ className }: { className?: string }) { } else { const hasNewer = entries.some((entry: ChangelogEntry) => { if (entry.isUnreleased) return false; - return isNewerCalVer(entry.version, lastSeen); + return isNewerVersion(entry.version, lastSeen); }); setHasNewVersions(hasNewer); } @@ -231,7 +234,7 @@ export function StackCompanion({ className }: { className?: string }) { } else { const hasNewer = changelogData.some((entry: ChangelogEntry) => { if (entry.isUnreleased) return false; - return isNewerCalVer(entry.version, lastSeen); + return isNewerVersion(entry.version, lastSeen); }); setHasNewVersions(hasNewer); } diff --git a/apps/dashboard/src/components/stack-companion/changelog-widget.tsx b/apps/dashboard/src/components/stack-companion/changelog-widget.tsx index 78bd0c0e22..5eed5f493a 100644 --- a/apps/dashboard/src/components/stack-companion/changelog-widget.tsx +++ b/apps/dashboard/src/components/stack-companion/changelog-widget.tsx @@ -2,11 +2,12 @@ import { Button } from '@/components/ui'; import { getPublicEnvVar } from '@/lib/env'; -import { CalendarIcon, CaretDownIcon, CaretUpIcon, InfoIcon } from '@phosphor-icons/react'; +import { CalendarIcon, CaretDownIcon, CaretUpIcon, InfoIcon, XIcon } from '@phosphor-icons/react'; import { captureError } from '@stackframe/stack-shared/dist/utils/errors'; import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; import Image from 'next/image'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -49,73 +50,73 @@ function markLatestVersionSeen(entries: ApiChangelogEntry[]) { const formatVersion = (version: string) => { - // Convert YYYY.MM.DD to YY.MM.DD format for display - const calVerMatch = version.match(/^(\d{4})\.(\d{2})\.(\d{2})$/); - if (calVerMatch) { - const [, year, month, day] = calVerMatch; - const shortYear = year.slice(-2); // Get last 2 digits - return `${shortYear}.${month}.${day}`; - } + // Version is already in US date format (M/D/YY), return as-is return version; }; -// Markdown component overrides for changelog rendering -const markdownComponents = { - h3: ({ children }: { children?: React.ReactNode }) => ( -
- {children} -
- ), - ul: ({ children }: { children?: React.ReactNode }) => ( -
- {children}
-
- ),
- blockquote: ({ children }: { children?: React.ReactNode }) => (
- + {children} +
+ ), + ul: ({ children }: { children?: React.ReactNode }) => ( +
+ {children}
+
+ ),
+ blockquote: ({ children }: { children?: React.ReactNode }) => (
+ - {error} -
- )} -- No changelog entries found -
++ {error} +
+ )} ++ No changelog entries found +