feat: remove the donation reminder and its column

This commit is contained in:
LukeGus committed 2026-10-09 08:06:22 -05:00
1 parent 0b41672a3c
commit abdbb34ce2
18 files changed
+14010 -105

No files matched your search

@@ -0,0 +1 @@
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -491,6 +491,13 @@
"when": 1791388888997,
"tag": "0069_plugin_channel",
"breakpoints": true
},
{
"idx": 70,
"version": "5",
"when": 1791550497926,
"tag": "0070_drop_donation_modal",
"breakpoints": true
}
]
}
@@ -0,0 +1 @@
ALTER TABLE "users" DROP COLUMN "donation_modal_dismissed";
File diff suppressed because it is too large. Load diff
+7
View File
@@ -491,6 +491,13 @@
"when": 1791388888397,
"tag": "0069_plugin_channel",
"breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1791550496661,
"tag": "0070_drop_donation_modal",
"breakpoints": true
}
]
}
@@ -0,0 +1 @@
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
File diff suppressed because it is too large. Load diff
+7
View File
@@ -456,6 +456,13 @@
"when": 1791388887785,
"tag": "0064_plugin_channel",
"breakpoints": true
},
{
"idx": 65,
"version": "6",
"when": 1791550495357,
"tag": "0065_drop_donation_modal",
"breakpoints": true
}
]
}
+2 -11
View File
@@ -242,8 +242,7 @@ async function initializeCompleteDatabase(): Promise<void> {
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
donation_modal_dismissed INTEGER NOT NULL DEFAULT 0
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS settings (
@@ -970,9 +969,7 @@ const migrateSchema = () => {
// CURRENT_TIMESTAMP, so the column is added empty and backfilled below.
addColumnIfNotExists("users", "registered_at", "TEXT");
if (!hadRegisteredAtColumn) {
// Pre-existing users are backdated past the 30 day mark so they see the
// donation modal immediately on upgrade instead of waiting a fresh
// 30 days as if they had just registered.
// Pre-existing users get a date in the past, since their real one is unknown.
try {
sqlite.exec(
`UPDATE users SET registered_at = datetime('now', '-31 days') WHERE registered_at IS NULL`,
@@ -1000,12 +997,6 @@ const migrateSchema = () => {
);
}
}
addColumnIfNotExists(
"users",
"donation_modal_dismissed",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists("sessions", "external_session_ref", "TEXT");
addColumnIfNotExists("ssh_data", "name", "TEXT");
@@ -77,6 +77,7 @@ export const RETIRED_COLUMNS: Record<string, string[]> = {
"identifier_path",
"name_path",
"scopes",
"donation_modal_dismissed",
],
sessions: ["oidc_sub", "oidc_sid", "sso_provider_id"],
user_preferences: [
-3
View File
@@ -27,9 +27,6 @@ export const users = mysqlTable("users", {
passwordHash: text("password_hash").notNull(),
isAdmin: boolean("is_admin").notNull().default(false),
registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`),
donationModalDismissed: boolean("donation_modal_dismissed")
.notNull()
.default(false),
});
export const settings = mysqlTable("settings", {
-3
View File
@@ -27,9 +27,6 @@ export const users = pgTable("users", {
passwordHash: text("password_hash").notNull(),
isAdmin: boolean("is_admin").notNull().default(false),
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
donationModalDismissed: boolean("donation_modal_dismissed")
.notNull()
.default(false),
});
export const settings = pgTable("settings", {
-5
View File
@@ -15,11 +15,6 @@ export const users = sqliteTable("users", {
passwordHash: text("password_hash").notNull(),
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
donationModalDismissed: integer("donation_modal_dismissed", {
mode: "boolean",
})
.notNull()
.default(false),
});
export const settings = sqliteTable("settings", {
-49
View File
@@ -20,7 +20,6 @@ import {
isNativeTokenExportRequest,
resolveDesktopAutoSessionUser,
} from "./desktop-auto-session.js";
import { shouldShowDonationModal } from "./donation-modal-utils.js";
import { PermissionManager } from "../../utils/permission-manager.js";
import { registerUserApiKeyRoutes } from "./user-api-key-routes.js";
import { registerBrandingRoutes } from "./branding-routes.js";
@@ -595,11 +594,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
const hasPassword = user.passwordHash && user.passwordHash.trim() !== "";
const isDualAuth = hasPassword && isExternalAccount(user);
const showDonationModal = shouldShowDonationModal(
user.registeredAt,
!!user.donationModalDismissed,
);
res.json({
userId: user.id,
username: user.username,
@@ -610,7 +604,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
user.id,
),
show_donation_modal: showDonationModal,
linked: await describeDesktopLink(user.id),
});
} catch (err) {
@@ -619,48 +612,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
}
});
/**
* @openapi
* /users/me/dismiss-donation-modal:
* post:
* summary: Permanently dismiss the donation reminder modal
* description: Marks the donation reminder modal as dismissed for the currently authenticated user so it is never shown to them again.
* tags:
* - Users
* responses:
* 200:
* description: Donation modal dismissed.
* 401:
* description: Invalid userId or user not found.
* 500:
* description: Failed to dismiss donation modal.
*/
router.post(
"/me/dismiss-donation-modal",
authenticateJWT,
async (req: Request, res: Response) => {
const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
return res.status(401).json({ error: "Invalid userId" });
}
try {
const updated = await createCurrentUserRepository().update(userId, {
donationModalDismissed: true,
});
if (!updated) {
return res.status(401).json({ error: "User not found" });
}
return res.json({ success: true });
} catch (err) {
authLogger.error("Failed to dismiss donation modal", err);
return res
.status(500)
.json({ error: "Failed to dismiss donation modal" });
}
},
);
/**
* @openapi
* /users/me/token:
+2 -18
View File
@@ -137,7 +137,6 @@ import {
getActiveSessions,
getUserPreferences,
saveUserPreferences,
dismissDonationModal,
type UserPreferences,
type OpenTabRecord,
} from "@/main-axios";
@@ -147,7 +146,6 @@ import {
resolveLayoutTabTarget,
snapshotData,
} from "@/shell/shell-layout";
import { DonationReminderModal } from "@/user/DonationReminderModal.tsx";
import { useSyncStatus } from "@/hooks/use-sync-status";
import { remScale } from "@/lib/rem";
import { dbHealthMonitor } from "@/lib/db-health-monitor";
@@ -304,8 +302,6 @@ export function AppShell({
// The standalone desktop backend still owns system settings such as the
// Tailscale API key, even though it has only one implicit user.
const showAdminUI = isAdmin;
const [showDonationModal, setShowDonationModal] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const [backgroundTabRecords, setBackgroundTabRecords] = useState<
OpenTabRecord[]
>([]);
@@ -444,18 +440,10 @@ export function AppShell({
useEffect(() => {
getUserInfo()
.then((info) => {
setIsAdmin(info.is_admin);
setShowDonationModal(!!info.show_donation_modal);
})
.then((info) => setIsAdmin(info.is_admin))
.catch(() => setIsAdmin(false));
}, []);
const handleDismissDonationModal = useCallback(() => {
setShowDonationModal(false);
dismissDonationModal().catch(() => {});
}, []);
const toggleAppFullscreen = useCallback(async () => {
try {
if (document.fullscreenElement) {
@@ -3192,11 +3180,7 @@ export function AppShell({
/>
</Suspense>
)}
<OnboardingHost onOpenChange={setShowOnboarding} />
<DonationReminderModal
open={showDonationModal && !showOnboarding}
onDismiss={handleDismissDonationModal}
/>
<OnboardingHost />
</ServerStatusProvider>
);
}
-7
View File
@@ -1130,13 +1130,6 @@
"customizeLayout": "Customize Dashboard",
"justNow": "just now"
},
"donation": {
"title": "Enjoying Termix?",
"body": "Termix is free and open source, built and maintained by a two-person team in our spare time. If it's replaced a commercial tool you'd otherwise be paying for, a donation helps cover hosting costs and keeps development going. As of now, donations are crypto only.",
"milestones": "Donations also help fund the time to research and learn what's needed to build features like SAML and Kubernetes support. See the progress on the donate page.",
"cta": "Donate",
"dismiss": "Maybe later"
},
"dashboardTab": {
"stable": "STABLE",
"registerNewServer": "Register a new server",
-9
View File
@@ -97,7 +97,6 @@ export interface UserInfo {
is_dual_auth?: boolean;
password_hash?: string;
data_unlocked?: boolean;
show_donation_modal?: boolean;
/** On a desktop linked to a server, the account it is signed in to there. */
linked?: LinkedAccountInfo | null;
}
@@ -1018,14 +1017,6 @@ export async function getUserInfo(): Promise<UserInfo> {
}
}
export async function dismissDonationModal(): Promise<void> {
try {
await authApi.post("/users/me/dismiss-donation-modal");
} catch (error) {
handleApiError(error, "dismiss donation modal");
}
}
export async function getCurrentToken(): Promise<string | null> {
try {
const response = await authApi.get("/users/me/token");