All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s
- upsellCtaFor(tier) decides what a locked screen offers: Base is
purchasable, Premium stays disabled with "Premium isn't sold online
yet". Pure and tested on the whole entitlements matrix.
- Shared CTAs, one component per file: PurchaseButton ("Get Base") and
DownloadLatestButton open the localized product page (purchaseUrl) and
need no router; HaveKeyButton renders only inside BrowserRouter.
- UpsellGate uses them; the purchase link never calls onNavigate, so
modal hosts stay open. ProfileFormModal reuses UpsellGate instead of
its compact panel, keeps its Cancel button, and closes after "I
already have a key" (onNavigate={onClose}).
- In Free, the Updates card offers Get Base, I already have a key and
Download the latest version; the error page (outside the router)
offers Get Base and the download link. No update check, no network
call.
- i18n FR/EN: Account card text, Premium note, Updates notEntitled text,
download label, editions guide step and tip; the "coming soon" note
key is gone. Tests pin the upsell key set and the decided texts.
- Docs: ADR 0017 amendment, architecture (UpsellGate row, shared CTAs,
upsellCtaFor, multi-profile row, counts), user guide section 12;
CHANGELOG FR/EN.
Proven red: Premium made purchasable 2, balance sold as Base in the
matrix 4, the old note key back in a locale 1, Account text altered 1,
download label altered 1.
Fixes #346
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { AlertTriangle, ChevronDown, ChevronUp, RefreshCw, Download, Mail, Bug } from "lucide-react";
|
|
import { check } from "@tauri-apps/plugin-updater";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import PurchaseButton from "./PurchaseButton";
|
|
import DownloadLatestButton from "./DownloadLatestButton";
|
|
|
|
interface ErrorPageProps {
|
|
error?: string;
|
|
}
|
|
|
|
export default function ErrorPage({ error }: ErrorPageProps) {
|
|
const { t } = useTranslation();
|
|
const [showDetails, setShowDetails] = useState(false);
|
|
const [updateStatus, setUpdateStatus] = useState<"idle" | "checking" | "available" | "upToDate" | "notEntitled" | "error">("idle");
|
|
const [updateVersion, setUpdateVersion] = useState<string | null>(null);
|
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
|
|
const handleCheckUpdate = async () => {
|
|
setUpdateStatus("checking");
|
|
setUpdateError(null);
|
|
try {
|
|
const allowed = await invoke<boolean>("check_entitlement", {
|
|
feature: "auto-update",
|
|
});
|
|
if (!allowed) {
|
|
setUpdateStatus("notEntitled");
|
|
return;
|
|
}
|
|
const update = await check();
|
|
if (update) {
|
|
setUpdateStatus("available");
|
|
setUpdateVersion(update.version);
|
|
} else {
|
|
setUpdateStatus("upToDate");
|
|
}
|
|
} catch (e) {
|
|
setUpdateStatus("error");
|
|
setUpdateError(e instanceof Error ? e.message : String(e));
|
|
}
|
|
};
|
|
|
|
const handleRefresh = () => {
|
|
window.location.reload();
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-[var(--background)] p-4">
|
|
<div className="max-w-md w-full space-y-6 text-center">
|
|
<AlertTriangle className="mx-auto h-16 w-16 text-[var(--destructive)]" />
|
|
|
|
<h1 className="text-2xl font-bold text-[var(--foreground)]">
|
|
{t("error.title")}
|
|
</h1>
|
|
|
|
{error && (
|
|
<div>
|
|
<button
|
|
onClick={() => setShowDetails(!showDetails)}
|
|
className="inline-flex items-center gap-1 text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
|
>
|
|
{showDetails ? t("error.hideDetails") : t("error.showDetails")}
|
|
{showDetails ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
</button>
|
|
{showDetails && (
|
|
<pre className="mt-2 p-3 bg-[var(--muted)] rounded-md text-xs text-left text-[var(--muted-foreground)] overflow-auto max-h-40">
|
|
{error}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<button
|
|
onClick={handleRefresh}
|
|
className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md bg-[var(--primary)] text-[var(--primary-foreground)] hover:opacity-90 transition-opacity"
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
{t("error.refresh")}
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleCheckUpdate}
|
|
disabled={updateStatus === "checking"}
|
|
className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)] transition-colors disabled:opacity-50"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
{updateStatus === "checking" ? t("common.loading") : t("error.checkUpdate")}
|
|
</button>
|
|
|
|
{updateStatus === "available" && updateVersion && (
|
|
<p className="text-sm text-[var(--primary)]">
|
|
{t("error.updateAvailable", { version: updateVersion })}
|
|
</p>
|
|
)}
|
|
{updateStatus === "upToDate" && (
|
|
<p className="text-sm text-[var(--muted-foreground)]">
|
|
{t("error.upToDate")}
|
|
</p>
|
|
)}
|
|
{/* Outside the router: no "I already have a key" here (#346). */}
|
|
{updateStatus === "notEntitled" && (
|
|
<div className="space-y-3">
|
|
<p className="text-sm text-[var(--muted-foreground)]">
|
|
{t("error.updateNotEntitled")}
|
|
</p>
|
|
<PurchaseButton />
|
|
<DownloadLatestButton />
|
|
</div>
|
|
)}
|
|
{updateStatus === "error" && updateError && (
|
|
<p className="text-sm text-[var(--destructive)]">{updateError}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="pt-4 border-t border-[var(--border)]">
|
|
<p className="text-sm font-medium text-[var(--foreground)] mb-3">
|
|
{t("error.contactUs")}
|
|
</p>
|
|
<div className="flex flex-col gap-2 text-sm">
|
|
<a
|
|
href="mailto:lacompagniemaximus@protonmail.com"
|
|
className="inline-flex items-center justify-center gap-2 text-[var(--primary)] hover:underline"
|
|
>
|
|
<Mail className="h-4 w-4" />
|
|
{t("error.contactEmail")} lacompagniemaximus@protonmail.com
|
|
</a>
|
|
<a
|
|
href="https://git.lacompagniemaximus.com/maximus/simpl-resultat/issues"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center justify-center gap-2 text-[var(--primary)] hover:underline"
|
|
>
|
|
<Bug className="h-4 w-4" />
|
|
{t("error.reportIssue")}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|