Merge branch 'V2' into feature/sign-placement-ui

This commit is contained in:
Reece Browne 2025-11-13 19:28:21 +00:00 committed by GitHub
commit 306117ed4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 462 additions and 46 deletions

View File

@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![83%](https://geps.dev/progress/83) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![32%](https://geps.dev/progress/32) |
| Basque (Euskara) (eu_ES) | ![18%](https://geps.dev/progress/18) |
| Bulgarian (Български) (bg_BG) | ![35%](https://geps.dev/progress/35) |
| Catalan (Català) (ca_CA) | ![34%](https://geps.dev/progress/34) |
| Croatian (Hrvatski) (hr_HR) | ![31%](https://geps.dev/progress/31) |
| Czech (Česky) (cs_CZ) | ![34%](https://geps.dev/progress/34) |
| Danish (Dansk) (da_DK) | ![30%](https://geps.dev/progress/30) |
| Dutch (Nederlands) (nl_NL) | ![30%](https://geps.dev/progress/30) |
| Arabic (العربية) (ar_AR) | ![64%](https://geps.dev/progress/64) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![24%](https://geps.dev/progress/24) |
| Basque (Euskara) (eu_ES) | ![14%](https://geps.dev/progress/14) |
| Bulgarian (Български) (bg_BG) | ![26%](https://geps.dev/progress/26) |
| Catalan (Català) (ca_CA) | ![26%](https://geps.dev/progress/26) |
| Croatian (Hrvatski) (hr_HR) | ![24%](https://geps.dev/progress/24) |
| Czech (Česky) (cs_CZ) | ![26%](https://geps.dev/progress/26) |
| Danish (Dansk) (da_DK) | ![23%](https://geps.dev/progress/23) |
| Dutch (Nederlands) (nl_NL) | ![23%](https://geps.dev/progress/23) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![82%](https://geps.dev/progress/82) |
| German (Deutsch) (de_DE) | ![84%](https://geps.dev/progress/84) |
| Greek (Ελληνικά) (el_GR) | ![34%](https://geps.dev/progress/34) |
| Hindi (हिंदी) (hi_IN) | ![34%](https://geps.dev/progress/34) |
| Hungarian (Magyar) (hu_HU) | ![38%](https://geps.dev/progress/38) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![31%](https://geps.dev/progress/31) |
| Irish (Gaeilge) (ga_IE) | ![34%](https://geps.dev/progress/34) |
| Italian (Italiano) (it_IT) | ![84%](https://geps.dev/progress/84) |
| Japanese (日本語) (ja_JP) | ![62%](https://geps.dev/progress/62) |
| Korean (한국어) (ko_KR) | ![34%](https://geps.dev/progress/34) |
| Norwegian (Norsk) (no_NB) | ![32%](https://geps.dev/progress/32) |
| Persian (فارسی) (fa_IR) | ![34%](https://geps.dev/progress/34) |
| Polish (Polski) (pl_PL) | ![36%](https://geps.dev/progress/36) |
| Portuguese (Português) (pt_PT) | ![34%](https://geps.dev/progress/34) |
| Portuguese Brazilian (Português) (pt_BR) | ![83%](https://geps.dev/progress/83) |
| Romanian (Română) (ro_RO) | ![28%](https://geps.dev/progress/28) |
| Russian (Русский) (ru_RU) | ![83%](https://geps.dev/progress/83) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![37%](https://geps.dev/progress/37) |
| Simplified Chinese (简体中文) (zh_CN) | ![85%](https://geps.dev/progress/85) |
| Slovakian (Slovensky) (sk_SK) | ![26%](https://geps.dev/progress/26) |
| Slovenian (Slovenščina) (sl_SI) | ![36%](https://geps.dev/progress/36) |
| Spanish (Español) (es_ES) | ![84%](https://geps.dev/progress/84) |
| Swedish (Svenska) (sv_SE) | ![33%](https://geps.dev/progress/33) |
| Thai (ไทย) (th_TH) | ![31%](https://geps.dev/progress/31) |
| French (Français) (fr_FR) | ![63%](https://geps.dev/progress/63) |
| German (Deutsch) (de_DE) | ![64%](https://geps.dev/progress/64) |
| Greek (Ελληνικά) (el_GR) | ![26%](https://geps.dev/progress/26) |
| Hindi (हिंदी) (hi_IN) | ![26%](https://geps.dev/progress/26) |
| Hungarian (Magyar) (hu_HU) | ![29%](https://geps.dev/progress/29) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![24%](https://geps.dev/progress/24) |
| Irish (Gaeilge) (ga_IE) | ![26%](https://geps.dev/progress/26) |
| Italian (Italiano) (it_IT) | ![64%](https://geps.dev/progress/64) |
| Japanese (日本語) (ja_JP) | ![47%](https://geps.dev/progress/47) |
| Korean (한국어) (ko_KR) | ![26%](https://geps.dev/progress/26) |
| Norwegian (Norsk) (no_NB) | ![24%](https://geps.dev/progress/24) |
| Persian (فارسی) (fa_IR) | ![26%](https://geps.dev/progress/26) |
| Polish (Polski) (pl_PL) | ![27%](https://geps.dev/progress/27) |
| Portuguese (Português) (pt_PT) | ![26%](https://geps.dev/progress/26) |
| Portuguese Brazilian (Português) (pt_BR) | ![64%](https://geps.dev/progress/64) |
| Romanian (Română) (ro_RO) | ![22%](https://geps.dev/progress/22) |
| Russian (Русский) (ru_RU) | ![63%](https://geps.dev/progress/63) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![28%](https://geps.dev/progress/28) |
| Simplified Chinese (简体中文) (zh_CN) | ![65%](https://geps.dev/progress/65) |
| Slovakian (Slovensky) (sk_SK) | ![19%](https://geps.dev/progress/19) |
| Slovenian (Slovenščina) (sl_SI) | ![27%](https://geps.dev/progress/27) |
| Spanish (Español) (es_ES) | ![64%](https://geps.dev/progress/64) |
| Swedish (Svenska) (sv_SE) | ![25%](https://geps.dev/progress/25) |
| Thai (ไทย) (th_TH) | ![23%](https://geps.dev/progress/23) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) |
| Traditional Chinese (繁體中文) (zh_TW) | ![38%](https://geps.dev/progress/38) |
| Turkish (Türkçe) (tr_TR) | ![37%](https://geps.dev/progress/37) |
| Ukrainian (Українська) (uk_UA) | ![36%](https://geps.dev/progress/36) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![28%](https://geps.dev/progress/28) |
| Traditional Chinese (繁體中文) (zh_TW) | ![29%](https://geps.dev/progress/29) |
| Turkish (Türkçe) (tr_TR) | ![28%](https://geps.dev/progress/28) |
| Ukrainian (Українська) (uk_UA) | ![28%](https://geps.dev/progress/28) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![21%](https://geps.dev/progress/21) |
| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) |
## Stirling PDF Enterprise

View File

@ -4678,6 +4678,12 @@
}
},
"apiKeys": {
"intro": "Use your API key to programmatically access Stirling PDF's processing capabilities.",
"docsTitle": "API Documentation",
"docsDescription": "Learn more about integrating with Stirling PDF:",
"docsLink": "API Documentation",
"schemaLink": "API Schema Reference",
"usage": "Include this key in the X-API-KEY header with all API requests.",
"description": "Your API key for accessing Stirling's suite of PDF tools. Copy it to your project or refresh to generate a new one.",
"publicKeyAriaLabel": "Public API key",
"copyKeyAriaLabel": "Copy API key",

View File

@ -64,17 +64,19 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
headerBorder: 'var(--modal-header-border)',
}), []);
// Get isAdmin and runningEE from app config
// Get isAdmin, runningEE, and loginEnabled from app config
const isAdmin = config?.isAdmin ?? false;
const runningEE = config?.runningEE ?? false;
const loginEnabled = config?.enableLogin ?? false;
// Left navigation structure and icons
const configNavSections = useMemo(() =>
createConfigNavSections(
isAdmin,
runningEE
runningEE,
loginEnabled
),
[isAdmin, runningEE]
[isAdmin, runningEE, loginEnabled]
);
const activeLabel = useMemo(() => {

View File

@ -41,7 +41,8 @@ export interface ConfigColors {
export const createConfigNavSections = (
isAdmin: boolean = false,
runningEE: boolean = false
runningEE: boolean = false,
_loginEnabled: boolean = false
): ConfigNavSection[] => {
const sections: ConfigNavSection[] = [
{

View File

@ -1,5 +1,5 @@
import { Group, Loader, Stack, Text } from '@mantine/core';
import { useMemo, useRef, useEffect } from 'react';
import { useMemo, useRef, useEffect, useState } from 'react';
import type { PagePreview } from '@app/types/compare';
import type { TokenBoundingBox, CompareDocumentPaneProps } from '@app/types/compare';
import { mergeConnectedRects, normalizeRotation, groupWordRects, computePageLayoutMetrics } from '@app/components/tools/compare/compare';
@ -53,6 +53,8 @@ const CompareDocumentPane = ({
// Track which page images have finished loading to avoid flashing between states
const imageLoadedRef = useRef<Map<number, boolean>>(new Map());
// Force a re-render when an image load state changes (refs don't trigger renders)
const [, setImageLoadedTick] = useState(0);
const visiblePageRafRef = useRef<number | null>(null);
const lastReportedVisiblePageRef = useRef<number | null>(null);
const pageNodesRef = useRef<HTMLElement[] | null>(null);
@ -252,6 +254,7 @@ const CompareDocumentPane = ({
onLoad={() => {
if (!imageLoadedRef.current.get(page.pageNumber)) {
imageLoadedRef.current.set(page.pageNumber, true);
setImageLoadedTick((v) => v + 1); // refs don't trigger renders
}
}}
/>

View File

@ -279,6 +279,15 @@
--modal-content-bg: #ffffff;
--modal-header-border: rgba(0, 0, 0, 0.06);
/* API Keys section colors (light mode) */
--api-keys-card-bg: #ffffff;
--api-keys-card-border: #e0e0e0;
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
--api-keys-input-bg: #f8f8f8;
--api-keys-input-border: #e0e0e0;
--api-keys-button-bg: #f5f5f5;
--api-keys-button-color: #333333;
/* PDF Report Colors (always light) */
--pdf-light-header-bg: 239 246 255;
--pdf-light-accent: 59 130 246;
@ -539,6 +548,15 @@
--modal-content-bg: #2A2F36;
--modal-header-border: rgba(255, 255, 255, 0.08);
/* API Keys section colors (dark mode) */
--api-keys-card-bg: #2A2F36;
--api-keys-card-border: #3A4047;
--api-keys-card-shadow: none;
--api-keys-input-bg: #1F2329;
--api-keys-input-border: #3A4047;
--api-keys-button-bg: #3A4047;
--api-keys-button-color: #D0D6DC;
/* Code token colors (dark mode - Cursor-like) */
--code-kw-color: #C792EA; /* purple */
--code-str-color: #C3E88D; /* green */

View File

@ -26,6 +26,57 @@ const Merge = (props: BaseToolProps) => {
props,
{ minFiles: 2 }
);
const naturalCompare = useCallback((a: string, b: string): number => {
const isDigit = (char: string) => char >= '0' && char <= '9';
const getChunk = (s: string, length: number, marker: number): { chunk: string; newMarker: number } => {
let chunk = '';
const c = s.charAt(marker);
chunk += c;
marker++;
if (isDigit(c)) {
while (marker < length && isDigit(s.charAt(marker))) {
chunk += s.charAt(marker);
marker++;
}
} else {
while (marker < length && !isDigit(s.charAt(marker))) {
chunk += s.charAt(marker);
marker++;
}
}
return { chunk, newMarker: marker };
};
const len1 = a.length;
const len2 = b.length;
let marker1 = 0;
let marker2 = 0;
while (marker1 < len1 && marker2 < len2) {
const { chunk: chunk1, newMarker: newMarker1 } = getChunk(a, len1, marker1);
marker1 = newMarker1;
const { chunk: chunk2, newMarker: newMarker2 } = getChunk(b, len2, marker2);
marker2 = newMarker2;
let result: number;
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
const num1 = parseInt(chunk1, 10);
const num2 = parseInt(chunk2, 10);
result = num1 - num2;
} else {
result = chunk1.localeCompare(chunk2);
}
if (result !== 0) {
return result;
}
}
return len1 - len2;
}, []);
// Custom file sorting logic for merge tool
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
@ -33,7 +84,7 @@ const Merge = (props: BaseToolProps) => {
let comparison = 0;
switch (sortType) {
case 'filename':
comparison = stubA.name.localeCompare(stubB.name);
comparison = naturalCompare(stubA.name, stubB.name);
break;
case 'dateModified':
comparison = stubA.lastModified - stubB.lastModified;
@ -45,7 +96,7 @@ const Merge = (props: BaseToolProps) => {
const selectedIds = sortedStubs.map(record => record.id);
const deselectedIds = fileIds.filter(id => !selectedIds.includes(id));
reorderFiles([...selectedIds, ...deselectedIds]);
}, [selectedFileStubs, fileIds, reorderFiles]);
}, [selectedFileStubs, fileIds, reorderFiles, naturalCompare]);
return createToolFlow({
files: {

View File

@ -2,13 +2,15 @@ import React from 'react';
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
/**
* Proprietary extension of createConfigNavSections that adds workspace sections
*/
export const createConfigNavSections = (
isAdmin: boolean = false,
runningEE: boolean = false
runningEE: boolean = false,
loginEnabled: boolean = false
): ConfigNavSection[] => {
// Get the core sections
const sections = createCoreConfigNavSections(isAdmin, runningEE);
@ -37,6 +39,25 @@ export const createConfigNavSections = (
sections.splice(1, 0, workspaceSection);
}
// Add Developer section if login is enabled
if (loginEnabled) {
const developerSection: ConfigNavSection = {
title: 'Developer',
items: [
{
key: 'api-keys',
label: 'API Keys',
icon: 'key-rounded',
component: <ApiKeys />
},
],
};
// Add Developer section after Preferences (or Workspace if it exists)
const insertIndex = isAdmin ? 2 : 1;
sections.splice(insertIndex, 0, developerSection);
}
return sections;
};

View File

@ -0,0 +1,126 @@
import React, { useState } from "react";
import { Anchor, Group, Stack, Text, Paper, Skeleton } from "@mantine/core";
// eslint-disable-next-line no-restricted-imports
import ApiKeySection from "./apiKeys/ApiKeySection";
// eslint-disable-next-line no-restricted-imports
import RefreshModal from "./apiKeys/RefreshModal";
// eslint-disable-next-line no-restricted-imports
import useApiKey from "./apiKeys/hooks/useApiKey";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
export default function ApiKeys() {
const [copied, setCopied] = useState<string | null>(null);
const [showRefreshModal, setShowRefreshModal] = useState(false);
const { t } = useTranslation();
const { apiKey, isLoading: apiKeyLoading, refresh, isRefreshing, error: apiKeyError, refetch } = useApiKey();
const copy = async (text: string, tag: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(tag);
setTimeout(() => setCopied(null), 1600);
} catch (e) {
console.error(e);
}
};
const refreshKeys = async () => {
try {
await refresh();
} finally {
setShowRefreshModal(false);
}
};
return (
<Stack gap={20} p={0}>
<Text size="sm" c="dimmed">
{t('config.apiKeys.intro', 'Use your API key to programmatically access Stirling PDF\'s processing capabilities.')}
</Text>
<Paper
p="md"
radius="md"
style={{
background: "var(--bg-muted)",
border: "1px solid var(--border-subtle)"
}}
>
<Group gap="xs" wrap="nowrap" align="flex-start">
<LocalIcon icon="info-rounded" width={18} height={18} style={{ marginTop: 2, flexShrink: 0, opacity: 0.7 }} />
<Stack gap={8} style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{t('config.apiKeys.docsTitle', 'API Documentation')}
</Text>
<Text size="sm" c="dimmed">
{t('config.apiKeys.docsDescription', 'Learn more about integrating with Stirling PDF:')}
</Text>
<Stack gap={4}>
<Text size="sm">
<Anchor
href="https://docs.stirlingpdf.com/API"
target="_blank"
rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
{t('config.apiKeys.docsLink', 'API Documentation')}
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
</Anchor>
</Text>
<Text size="sm">
<Anchor
href="https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/"
target="_blank"
rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
>
{t('config.apiKeys.schemaLink', 'API Schema Reference')}
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
</Anchor>
</Text>
</Stack>
</Stack>
</Group>
</Paper>
{apiKeyError && (
<Text size="sm" c="red.5">
{t('config.apiKeys.generateError', "We couldn't generate your API key.")} {" "}
<Anchor component="button" underline="always" onClick={refetch} c="red.4">
{t('common.retry', 'Retry')}
</Anchor>
</Text>
)}
{apiKeyLoading ? (
<div style={{ padding: 18, borderRadius: 12, background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
<Group align="center" gap={12} wrap="nowrap">
<Skeleton height={36} style={{ flex: 1 }} />
<Skeleton height={32} width={76} />
<Skeleton height={32} width={92} />
</Group>
</div>
) : (
<ApiKeySection
publicKey={apiKey ?? ""}
copied={copied}
onCopy={copy}
onRefresh={() => setShowRefreshModal(true)}
disabled={isRefreshing}
/>
)}
<Text size="sm" c="dimmed" style={{ marginTop: -8 }}>
{t('config.apiKeys.usage', 'Include this key in the X-API-KEY header with all API requests.')}
</Text>
<RefreshModal
opened={showRefreshModal}
onClose={() => setShowRefreshModal(false)}
onConfirm={refreshKeys}
/>
</Stack>
);
}

View File

@ -0,0 +1,75 @@
import React from "react";
import {
Box,
Button,
Group,
Paper,
} from "@mantine/core";
import LocalIcon from "@app/components/shared/LocalIcon";
import FitText from "@app/components/shared/FitText";
import { useTranslation } from "react-i18next";
interface ApiKeySectionProps {
publicKey: string;
copied: string | null;
onCopy: (text: string, tag: string) => void;
onRefresh: () => void;
disabled?: boolean;
}
export default function ApiKeySection({
publicKey,
copied,
onCopy,
onRefresh,
disabled,
}: ApiKeySectionProps) {
const { t } = useTranslation();
return (
<>
<Paper radius="md" p={18} style={{ background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
<Group align="flex-end" wrap="nowrap">
<Box style={{ flex: 1 }}>
<Box
style={{
background: "var(--api-keys-input-bg)",
border: "1px solid var(--api-keys-input-border)",
borderRadius: 8,
padding: "8px 12px",
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 14,
minHeight: 36,
display: "flex",
alignItems: "center",
}}
aria-label={t('config.apiKeys.publicKeyAriaLabel', 'Public API key')}
>
<FitText text={publicKey} />
</Box>
</Box>
<Button
size="sm"
variant="light"
onClick={() => onCopy(publicKey, "public")}
leftSection={<LocalIcon icon="content-copy-rounded" width={14} height={14} />}
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 12 } }}
aria-label={t('config.apiKeys.copyKeyAriaLabel', 'Copy API key')}
>
{copied === "public" ? t('common.copied', 'Copied!') : t('common.copy', 'Copy')}
</Button>
<Button
size="sm"
variant="light"
onClick={onRefresh}
leftSection={<LocalIcon icon="refresh-rounded" width={14} height={14} />}
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 8 } }}
disabled={disabled}
aria-label={t('config.apiKeys.refreshAriaLabel', 'Refresh API key')}
>
{t('common.refresh', 'Refresh')}
</Button>
</Group>
</Paper>
</>
);
}

View File

@ -0,0 +1,50 @@
import React from "react";
import {
Modal,
Stack,
Text,
Group,
Button,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
interface RefreshModalProps {
opened: boolean;
onClose: () => void;
onConfirm: () => void;
}
export default function RefreshModal({ opened, onClose, onConfirm }: RefreshModalProps) {
const { t } = useTranslation();
return (
<Modal
opened={opened}
onClose={onClose}
title={t('config.apiKeys.refreshModal.title', 'Refresh API Keys')}
centered
size="sm"
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
>
<Stack gap="md">
<Text size="sm" c="red">
{t('config.apiKeys.refreshModal.warning', '⚠️ Warning: This action will generate new API keys and make your previous keys invalid.')}
</Text>
<Text size="sm">
{t('config.apiKeys.refreshModal.impact', 'Any applications or services currently using these keys will stop working until you update them with the new keys.')}
</Text>
<Text size="sm" fw={500}>
{t('config.apiKeys.refreshModal.confirmPrompt', 'Are you sure you want to continue?')}
</Text>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={onClose}>
{t('common.cancel', 'Cancel')}
</Button>
<Button color="red" onClick={onConfirm}>
{t('config.apiKeys.refreshModal.confirmCta', 'Refresh Keys')}
</Button>
</Group>
</Stack>
</Modal>
);
}

View File

@ -0,0 +1,64 @@
import { useCallback, useEffect, useState } from "react";
import apiClient from "@app/services/apiClient";
export function useApiKey() {
const [apiKey, setApiKey] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
const fetchKey = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Backend is POST for get and update
const res = await apiClient.post("/api/v1/user/get-api-key");
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
// If not found, try to create one by calling update endpoint
if (e?.response?.status === 404) {
try {
const createRes = await apiClient.post("/api/v1/user/update-api-key");
const created =
typeof createRes.data === "string"
? createRes.data
: createRes.data?.apiKey;
if (typeof created === "string") setApiKey(created);
} catch (createErr: any) {
setError(createErr);
}
} else {
setError(e);
}
} finally {
setIsLoading(false);
setHasAttempted(true);
}
}, []);
const refresh = useCallback(async () => {
setIsRefreshing(true);
setError(null);
try {
const res = await apiClient.post("/api/v1/user/update-api-key");
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
setError(e);
} finally {
setIsRefreshing(false);
}
}, []);
useEffect(() => {
if (!hasAttempted) {
fetchKey();
}
}, [hasAttempted, fetchKey]);
return { apiKey, isLoading, isRefreshing, error, refetch: fetchKey, refresh, hasAttempted } as const;
}
export default useApiKey;

View File

@ -242,7 +242,6 @@ ignore = [
'team.status',
'text',
'update.version',
'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1',