Adds the user-facing layer on top of the Rust license commands shipped in #46. - `licenseService.ts` thin wrapper around the new Tauri commands - `useLicense` hook follows the project's useReducer pattern (idle, loading, ready, validating, error) and exposes `submitKey`, `refresh`, and `checkEntitlement` for cross-component use - `LicenseCard` shows the current edition, the expiry date when set, accepts a license key with inline validation feedback, and links to the purchase page via `openUrl` from `@tauri-apps/plugin-opener` - Card is inserted at the top of `SettingsPage` so the edition is the first thing users see when looking for license-related actions - i18n: new `license.*` keys in both `fr.json` and `en.json` - Bilingual CHANGELOG entries
98 lines
2.5 KiB
TypeScript
98 lines
2.5 KiB
TypeScript
import { useCallback, useEffect, useReducer } from "react";
|
|
import {
|
|
Edition,
|
|
LicenseInfo,
|
|
checkEntitlement as checkEntitlementCmd,
|
|
getEdition,
|
|
readLicense,
|
|
storeLicense,
|
|
} from "../services/licenseService";
|
|
|
|
type LicenseStatus = "idle" | "loading" | "ready" | "validating" | "error";
|
|
|
|
interface LicenseState {
|
|
status: LicenseStatus;
|
|
edition: Edition;
|
|
info: LicenseInfo | null;
|
|
error: string | null;
|
|
}
|
|
|
|
type LicenseAction =
|
|
| { type: "LOAD_START" }
|
|
| { type: "LOAD_DONE"; edition: Edition; info: LicenseInfo | null }
|
|
| { type: "VALIDATE_START" }
|
|
| { type: "VALIDATE_DONE"; info: LicenseInfo }
|
|
| { type: "ERROR"; error: string };
|
|
|
|
const initialState: LicenseState = {
|
|
status: "idle",
|
|
edition: "free",
|
|
info: null,
|
|
error: null,
|
|
};
|
|
|
|
function reducer(state: LicenseState, action: LicenseAction): LicenseState {
|
|
switch (action.type) {
|
|
case "LOAD_START":
|
|
return { ...state, status: "loading", error: null };
|
|
case "LOAD_DONE":
|
|
return {
|
|
status: "ready",
|
|
edition: action.edition,
|
|
info: action.info,
|
|
error: null,
|
|
};
|
|
case "VALIDATE_START":
|
|
return { ...state, status: "validating", error: null };
|
|
case "VALIDATE_DONE":
|
|
return {
|
|
status: "ready",
|
|
edition: action.info.edition,
|
|
info: action.info,
|
|
error: null,
|
|
};
|
|
case "ERROR":
|
|
return { ...state, status: "error", error: action.error };
|
|
}
|
|
}
|
|
|
|
export function useLicense() {
|
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
|
|
const refresh = useCallback(async () => {
|
|
dispatch({ type: "LOAD_START" });
|
|
try {
|
|
const [edition, info] = await Promise.all([getEdition(), readLicense()]);
|
|
dispatch({ type: "LOAD_DONE", edition, info });
|
|
} catch (e) {
|
|
dispatch({
|
|
type: "ERROR",
|
|
error: e instanceof Error ? e.message : String(e),
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
const submitKey = useCallback(async (key: string) => {
|
|
dispatch({ type: "VALIDATE_START" });
|
|
try {
|
|
const info = await storeLicense(key);
|
|
dispatch({ type: "VALIDATE_DONE", info });
|
|
return { ok: true as const, info };
|
|
} catch (e) {
|
|
const message = e instanceof Error ? e.message : String(e);
|
|
dispatch({ type: "ERROR", error: message });
|
|
return { ok: false as const, error: message };
|
|
}
|
|
}, []);
|
|
|
|
const checkEntitlement = useCallback(
|
|
(feature: string) => checkEntitlementCmd(feature),
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
void refresh();
|
|
}, [refresh]);
|
|
|
|
return { state, refresh, submitKey, checkEntitlement };
|
|
}
|