@greatapps/common 1.1.730 → 1.1.731
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +22 -3
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/modules/whitelabel/utils/normalize-hostname.mjs +1 -0
- package/dist/modules/whitelabel/utils/normalize-hostname.mjs.map +1 -1
- package/dist/store/useDesktopSidebarStore.mjs +5 -4
- package/dist/store/useDesktopSidebarStore.mjs.map +1 -1
- package/dist/store/useMdSidebarStore.mjs +5 -7
- package/dist/store/useMdSidebarStore.mjs.map +1 -1
- package/dist/utils/browser/safe-storage.mjs +20 -0
- package/dist/utils/browser/safe-storage.mjs.map +1 -0
- package/package.json +1 -1
- package/src/modules/whitelabel/services/whitelabel.service.ts +29 -5
- package/src/modules/whitelabel/utils/normalize-hostname.ts +1 -0
- package/src/store/useDesktopSidebarStore.ts +5 -4
- package/src/store/useMdSidebarStore.ts +5 -7
- package/src/utils/browser/safe-storage.ts +20 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import greatCache from "@greatapps/cache";
|
|
2
2
|
import { ApiError } from "../../../infra/api/types";
|
|
3
3
|
import { normalizeHostname } from "../utils/normalize-hostname";
|
|
4
|
+
const NOT_FOUND_SENTINEL = "__WL_TOKEN_NOT_FOUND__";
|
|
4
5
|
class WhitelabelService {
|
|
5
6
|
getApiUrl() {
|
|
6
7
|
const apiUrl = process.env.GAPPS_R3_API_URL;
|
|
@@ -47,6 +48,10 @@ class WhitelabelService {
|
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
if (!response.ok) {
|
|
51
|
+
const body = await response.json().catch(() => null);
|
|
52
|
+
if (response.status === 404 || body?.message === "Whitelabel not found") {
|
|
53
|
+
throw new ApiError("Whitelabel token not found", "TOKEN_NOT_FOUND", 404);
|
|
54
|
+
}
|
|
50
55
|
console.error("[WhitelabelService] Failed to fetch whitelabel token", {
|
|
51
56
|
response
|
|
52
57
|
});
|
|
@@ -76,12 +81,26 @@ class WhitelabelService {
|
|
|
76
81
|
cachedData
|
|
77
82
|
});
|
|
78
83
|
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
84
|
+
if (cachedData.data === NOT_FOUND_SENTINEL) {
|
|
85
|
+
console.log("[WhitelabelService] Negative cache hit for domain", { hostname });
|
|
86
|
+
throw new ApiError("Whitelabel token not found", "TOKEN_NOT_FOUND", 404);
|
|
87
|
+
}
|
|
79
88
|
console.log("[WhitelabelService] Cache hit for domain", { hostname });
|
|
80
89
|
return JSON.parse(cachedData.data);
|
|
81
90
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
try {
|
|
92
|
+
const data = await this.fetchFromApi(hostname);
|
|
93
|
+
await cache.insert(cacheKey, JSON.stringify(data), 604800);
|
|
94
|
+
return data;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (ApiError.isApiError(error) && error.code === "TOKEN_NOT_FOUND") {
|
|
97
|
+
try {
|
|
98
|
+
await cache.insert(cacheKey, NOT_FOUND_SENTINEL, 60);
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
85
104
|
}
|
|
86
105
|
async getTokenByWhitelabelId(idWl) {
|
|
87
106
|
const apiUrl = this.getApiUrl();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["import greatCache from \"@greatapps/cache\";\r\nimport { ApiError } from \"../../../infra/api/types\";\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from \"../schema\";\r\nimport { normalizeHostname } from \"../utils/normalize-hostname\";\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.GAPPS_R3_API_URL;\r\n if (!apiUrl) {\r\n throw new ApiError(\r\n \"GAPPS_R3_API_URL not configured\",\r\n \"CONFIG_ERROR\",\r\n 500,\r\n );\r\n }\r\n return apiUrl;\r\n }\r\n\r\n private getToken(): string {\r\n const token = process.env.WHITELABEL_TOKEN_MASTER;\r\n if (!token) {\r\n throw new ApiError(\r\n \"WHITELABEL_TOKEN_MASTER not configured\",\r\n \"CONFIG_ERROR\",\r\n 500,\r\n );\r\n }\r\n return token;\r\n }\r\n\r\n private createCache() {\r\n return new greatCache({\r\n service: \"whitelabel-service\",\r\n version: \"1.0.3\",\r\n domain: \"whitelabel-cache.greatapps.com.br\",\r\n ambient: process.env.NODE_ENV || \"development\",\r\n });\r\n }\r\n\r\n private getCacheKey(hostname: string): string {\r\n return `whitelabel-token-${hostname}`;\r\n }\r\n\r\n private async fetchFromApi(hostname: string): Promise<WhitelabelTokenData> {\r\n const apiUrl = this.getApiUrl();\r\n const whitelabelMasterToken = this.getToken();\r\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\r\n\r\n console.log(\"[WhitelabelService] Fetching token for domain\", { url });\r\n\r\n const response = await fetch(url, {\r\n method: \"GET\",\r\n headers: {\r\n authorization: whitelabelMasterToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n console.error(\"[WhitelabelService] Failed to fetch whitelabel token\", {\r\n response,\r\n });\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n \"FETCH_ERROR\",\r\n response.status,\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError(\"Whitelabel token not found\", \"TOKEN_NOT_FOUND\", 404);\r\n }\r\n\r\n return result.data[0];\r\n }\r\n\r\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\r\n console.debug(\"[WhitelabelService] Getting token for domain\", {\r\n hostname,\r\n byEnv: process.env.WHITELABEL_DOMAIN,\r\n });\r\n\r\n const cache = this.createCache();\r\n const cacheKey = this.getCacheKey(hostname);\r\n\r\n const cachedData = await cache.select(cacheKey);\r\n console.debug(\"[WhitelabelService] Cache lookup for domain\", {\r\n cacheKey,\r\n cachedData,\r\n });\r\n\r\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\r\n console.log(\"[WhitelabelService] Cache hit for domain\", { hostname });\r\n return JSON.parse(cachedData.data) as WhitelabelTokenData;\r\n }\r\n\r\n const data = await this.fetchFromApi(hostname);\r\n\r\n
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["import greatCache from \"@greatapps/cache\";\r\nimport { ApiError } from \"../../../infra/api/types\";\r\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from \"../schema\";\r\nimport { normalizeHostname } from \"../utils/normalize-hostname\";\r\n\r\nconst NOT_FOUND_SENTINEL = \"__WL_TOKEN_NOT_FOUND__\";\r\n\r\nclass WhitelabelService {\r\n private getApiUrl(): string {\r\n const apiUrl = process.env.GAPPS_R3_API_URL;\r\n if (!apiUrl) {\r\n throw new ApiError(\r\n \"GAPPS_R3_API_URL not configured\",\r\n \"CONFIG_ERROR\",\r\n 500,\r\n );\r\n }\r\n return apiUrl;\r\n }\r\n\r\n private getToken(): string {\r\n const token = process.env.WHITELABEL_TOKEN_MASTER;\r\n if (!token) {\r\n throw new ApiError(\r\n \"WHITELABEL_TOKEN_MASTER not configured\",\r\n \"CONFIG_ERROR\",\r\n 500,\r\n );\r\n }\r\n return token;\r\n }\r\n\r\n private createCache() {\r\n return new greatCache({\r\n service: \"whitelabel-service\",\r\n version: \"1.0.3\",\r\n domain: \"whitelabel-cache.greatapps.com.br\",\r\n ambient: process.env.NODE_ENV || \"development\",\r\n });\r\n }\r\n\r\n private getCacheKey(hostname: string): string {\r\n return `whitelabel-token-${hostname}`;\r\n }\r\n\r\n private async fetchFromApi(hostname: string): Promise<WhitelabelTokenData> {\r\n const apiUrl = this.getApiUrl();\r\n const whitelabelMasterToken = this.getToken();\r\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\r\n\r\n console.log(\"[WhitelabelService] Fetching token for domain\", { url });\r\n\r\n const response = await fetch(url, {\r\n method: \"GET\",\r\n headers: {\r\n authorization: whitelabelMasterToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n const body = await response.json().catch(() => null);\r\n\r\n if (response.status === 404 || body?.message === \"Whitelabel not found\") {\r\n throw new ApiError(\"Whitelabel token not found\", \"TOKEN_NOT_FOUND\", 404);\r\n }\r\n\r\n console.error(\"[WhitelabelService] Failed to fetch whitelabel token\", {\r\n response,\r\n });\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n \"FETCH_ERROR\",\r\n response.status,\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError(\"Whitelabel token not found\", \"TOKEN_NOT_FOUND\", 404);\r\n }\r\n\r\n return result.data[0];\r\n }\r\n\r\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\r\n console.debug(\"[WhitelabelService] Getting token for domain\", {\r\n hostname,\r\n byEnv: process.env.WHITELABEL_DOMAIN,\r\n });\r\n\r\n const cache = this.createCache();\r\n const cacheKey = this.getCacheKey(hostname);\r\n\r\n const cachedData = await cache.select(cacheKey);\r\n console.debug(\"[WhitelabelService] Cache lookup for domain\", {\r\n cacheKey,\r\n cachedData,\r\n });\r\n\r\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\r\n if (cachedData.data === NOT_FOUND_SENTINEL) {\r\n console.log(\"[WhitelabelService] Negative cache hit for domain\", { hostname });\r\n throw new ApiError(\"Whitelabel token not found\", \"TOKEN_NOT_FOUND\", 404);\r\n }\r\n\r\n console.log(\"[WhitelabelService] Cache hit for domain\", { hostname });\r\n return JSON.parse(cachedData.data) as WhitelabelTokenData;\r\n }\r\n\r\n try {\r\n const data = await this.fetchFromApi(hostname);\r\n\r\n await cache.insert(cacheKey, JSON.stringify(data), 604800);\r\n\r\n return data;\r\n } catch (error) {\r\n if (ApiError.isApiError(error) && error.code === \"TOKEN_NOT_FOUND\") {\r\n try {\r\n await cache.insert(cacheKey, NOT_FOUND_SENTINEL, 60);\r\n } catch {\r\n // falha do cache não pode substituir o TOKEN_NOT_FOUND original\r\n }\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n async getTokenByWhitelabelId(idWl: number): Promise<string> {\r\n const apiUrl = this.getApiUrl();\r\n const masterToken = this.getToken();\r\n const url = `${apiUrl}/v1/pt-br/${idWl}/tokens?limit=1&page=1&sort=id:desc`;\r\n console.debug(\"[WhitelabelService] Fetching token by whitelabel ID\", {\r\n url,\r\n config: {\r\n method: \"GET\",\r\n headers: { authorization: masterToken },\r\n },\r\n });\r\n\r\n const response = await fetch(url, {\r\n method: \"GET\",\r\n headers: { authorization: masterToken },\r\n });\r\n\r\n console.debug(\"[WhitelabelService] Response received for whitelabel ID\", {\r\n response,\r\n });\r\n\r\n if (!response.ok) {\r\n throw new ApiError(\r\n `Failed to fetch whitelabel token: ${response.status}`,\r\n \"WL_TOKEN_NOT_FOUND\",\r\n response.status,\r\n );\r\n }\r\n\r\n const result: WhitelabelTokenApiResponse = await response.json();\r\n\r\n if (result.status !== 1 || !result.data?.length) {\r\n throw new ApiError(\r\n \"Token do whitelabel não encontrado\",\r\n \"WL_TOKEN_NOT_FOUND\",\r\n 404,\r\n );\r\n }\r\n\r\n return result.data[0].token;\r\n }\r\n\r\n async revalidateByDomain(hostname: string): Promise<WhitelabelTokenData> {\r\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\r\n console.debug(\"[WhitelabelService] Getting token for domain\", {\r\n hostname,\r\n byEnv: process.env.WHITELABEL_DOMAIN,\r\n });\r\n\r\n const cache = this.createCache();\r\n const cacheKey = this.getCacheKey(hostname);\r\n\r\n console.log(\"[WhitelabelService] Revalidating cache for domain\", {\r\n hostname,\r\n cacheKey,\r\n });\r\n \r\n await cache.delete(cacheKey);\r\n\r\n const data = await this.fetchFromApi(hostname);\r\n\r\n await cache.insert(cacheKey, JSON.stringify(data), 604800);\r\n\r\n console.log(\"[WhitelabelService] Cache revalidated for domain\", {\r\n hostname,\r\n });\r\n\r\n return data;\r\n }\r\n}\r\n\r\nexport const whitelabelService = new WhitelabelService();\r\n"],"mappings":"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","names":[]}
|
|
@@ -17,6 +17,7 @@ const CATEGORY_PREFIXES = /* @__PURE__ */ new Set([
|
|
|
17
17
|
]);
|
|
18
18
|
const SIMPLE_TLDS = /* @__PURE__ */ new Set(["online"]);
|
|
19
19
|
function normalizeHostname(hostname) {
|
|
20
|
+
hostname = hostname.split(":")[0];
|
|
20
21
|
const parts = hostname.split(".");
|
|
21
22
|
if (parts.length <= 2) return hostname;
|
|
22
23
|
const last = parts[parts.length - 1];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/whitelabel/utils/normalize-hostname.ts"],"sourcesContent":["const CATEGORY_PREFIXES = new Set([\n \"com\", \"dev\", \"net\", \"org\", \"edu\", \"gov\",\n \"mil\", \"co\", \"art\", \"inf\", \"blog\", \"wiki\", \"app\", \"online\", \"site\"\n]);\n\nconst SIMPLE_TLDS = new Set([\"online\"]);\n\nexport function normalizeHostname(hostname: string): string {\n const parts = hostname.split(\".\");\n if (parts.length <= 2) return hostname;\n\n const last = parts[parts.length - 1];\n const secondLast = parts[parts.length - 2];\n\n const isCompoundTld =\n last.length <= 3 && CATEGORY_PREFIXES.has(secondLast);\n\n const rootParts = isCompoundTld ? 3 : 2;\n\n if (SIMPLE_TLDS.has(last) || !isCompoundTld) {\n return parts.slice(-2).join(\".\");\n }\n\n if (parts.length <= rootParts) return hostname;\n return parts.slice(-rootParts).join(\".\");\n}\n"],"mappings":"AAAA,MAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EACnC;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAC9D,CAAC;AAED,MAAM,cAAc,oBAAI,IAAI,CAAC,QAAQ,CAAC;AAE/B,SAAS,kBAAkB,UAA0B;AAC1D,QAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,MAAI,MAAM,UAAU,EAAG,QAAO;AAE9B,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAM,aAAa,MAAM,MAAM,SAAS,CAAC;AAEzC,QAAM,gBACJ,KAAK,UAAU,KAAK,kBAAkB,IAAI,UAAU;AAEtD,QAAM,YAAY,gBAAgB,IAAI;AAEtC,MAAI,YAAY,IAAI,IAAI,KAAK,CAAC,eAAe;AAC3C,WAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAAA,EACjC;AAEA,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,MAAM,MAAM,CAAC,SAAS,EAAE,KAAK,GAAG;AACzC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/whitelabel/utils/normalize-hostname.ts"],"sourcesContent":["const CATEGORY_PREFIXES = new Set([\n \"com\", \"dev\", \"net\", \"org\", \"edu\", \"gov\",\n \"mil\", \"co\", \"art\", \"inf\", \"blog\", \"wiki\", \"app\", \"online\", \"site\"\n]);\n\nconst SIMPLE_TLDS = new Set([\"online\"]);\n\nexport function normalizeHostname(hostname: string): string {\n hostname = hostname.split(\":\")[0];\n const parts = hostname.split(\".\");\n if (parts.length <= 2) return hostname;\n\n const last = parts[parts.length - 1];\n const secondLast = parts[parts.length - 2];\n\n const isCompoundTld =\n last.length <= 3 && CATEGORY_PREFIXES.has(secondLast);\n\n const rootParts = isCompoundTld ? 3 : 2;\n\n if (SIMPLE_TLDS.has(last) || !isCompoundTld) {\n return parts.slice(-2).join(\".\");\n }\n\n if (parts.length <= rootParts) return hostname;\n return parts.slice(-rootParts).join(\".\");\n}\n"],"mappings":"AAAA,MAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EACnC;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAC9D,CAAC;AAED,MAAM,cAAc,oBAAI,IAAI,CAAC,QAAQ,CAAC;AAE/B,SAAS,kBAAkB,UAA0B;AAC1D,aAAW,SAAS,MAAM,GAAG,EAAE,CAAC;AAChC,QAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,MAAI,MAAM,UAAU,EAAG,QAAO;AAE9B,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAM,aAAa,MAAM,MAAM,SAAS,CAAC;AAEzC,QAAM,gBACJ,KAAK,UAAU,KAAK,kBAAkB,IAAI,UAAU;AAEtD,QAAM,YAAY,gBAAgB,IAAI;AAEtC,MAAI,YAAY,IAAI,IAAI,KAAK,CAAC,eAAe;AAC3C,WAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAAA,EACjC;AAEA,MAAI,MAAM,UAAU,UAAW,QAAO;AACtC,SAAO,MAAM,MAAM,CAAC,SAAS,EAAE,KAAK,GAAG;AACzC;","names":[]}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
+
import { readStorage, writeStorage } from "../utils/browser/safe-storage";
|
|
3
4
|
const STORAGE_KEY = "desktop-sidebar-expanded";
|
|
4
5
|
const useDesktopSidebarStore = create((set) => ({
|
|
5
6
|
isExpanded: true,
|
|
6
7
|
_hydrated: false,
|
|
7
8
|
toggle: () => set((state) => {
|
|
8
9
|
const next = !state.isExpanded;
|
|
9
|
-
|
|
10
|
+
writeStorage(STORAGE_KEY, String(next));
|
|
10
11
|
return { isExpanded: next };
|
|
11
12
|
}),
|
|
12
13
|
expand: () => {
|
|
13
|
-
|
|
14
|
+
writeStorage(STORAGE_KEY, "true");
|
|
14
15
|
return set({ isExpanded: true });
|
|
15
16
|
},
|
|
16
17
|
collapse: () => {
|
|
17
|
-
|
|
18
|
+
writeStorage(STORAGE_KEY, "false");
|
|
18
19
|
return set({ isExpanded: false });
|
|
19
20
|
},
|
|
20
21
|
_hydrate: () => {
|
|
21
|
-
const stored =
|
|
22
|
+
const stored = readStorage(STORAGE_KEY);
|
|
22
23
|
const value = stored === null ? true : stored === "true";
|
|
23
24
|
set({ isExpanded: value, _hydrated: true });
|
|
24
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/store/useDesktopSidebarStore.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { useEffect } from \"react\";\n\nconst STORAGE_KEY = \"desktop-sidebar-expanded\";\n\ntype DesktopSidebarStore = {\n isExpanded: boolean;\n _hydrated: boolean;\n toggle: () => void;\n expand: () => void;\n collapse: () => void;\n _hydrate: () => void;\n};\n\nexport const useDesktopSidebarStore = create<DesktopSidebarStore>((set) => ({\n isExpanded: true,\n _hydrated: false,\n toggle: () => set((state) => {\n const next = !state.isExpanded;\n
|
|
1
|
+
{"version":3,"sources":["../../src/store/useDesktopSidebarStore.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { useEffect } from \"react\";\nimport { readStorage, writeStorage } from \"../utils/browser/safe-storage\";\n\nconst STORAGE_KEY = \"desktop-sidebar-expanded\";\n\ntype DesktopSidebarStore = {\n isExpanded: boolean;\n _hydrated: boolean;\n toggle: () => void;\n expand: () => void;\n collapse: () => void;\n _hydrate: () => void;\n};\n\nexport const useDesktopSidebarStore = create<DesktopSidebarStore>((set) => ({\n isExpanded: true,\n _hydrated: false,\n toggle: () => set((state) => {\n const next = !state.isExpanded;\n writeStorage(STORAGE_KEY, String(next));\n return { isExpanded: next };\n }),\n expand: () => {\n writeStorage(STORAGE_KEY, \"true\");\n return set({ isExpanded: true });\n },\n collapse: () => {\n writeStorage(STORAGE_KEY, \"false\");\n return set({ isExpanded: false });\n },\n _hydrate: () => {\n const stored = readStorage(STORAGE_KEY);\n const value = stored === null ? true : stored === \"true\";\n set({ isExpanded: value, _hydrated: true });\n },\n}));\n\nexport function useDesktopSidebarHydration() {\n const hydrate = useDesktopSidebarStore((s) => s._hydrate);\n const hydrated = useDesktopSidebarStore((s) => s._hydrated);\n\n useEffect(() => {\n if (!hydrated) {\n hydrate();\n }\n }, [hydrate, hydrated]);\n}\n"],"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,iBAAiB;AAC1B,SAAS,aAAa,oBAAoB;AAE1C,MAAM,cAAc;AAWb,MAAM,yBAAyB,OAA4B,CAAC,SAAS;AAAA,EACxE,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ,MAAM,IAAI,CAAC,UAAU;AACzB,UAAM,OAAO,CAAC,MAAM;AACpB,iBAAa,aAAa,OAAO,IAAI,CAAC;AACtC,WAAO,EAAE,YAAY,KAAK;AAAA,EAC9B,CAAC;AAAA,EACD,QAAQ,MAAM;AACV,iBAAa,aAAa,MAAM;AAChC,WAAO,IAAI,EAAE,YAAY,KAAK,CAAC;AAAA,EACnC;AAAA,EACA,UAAU,MAAM;AACZ,iBAAa,aAAa,OAAO;AACjC,WAAO,IAAI,EAAE,YAAY,MAAM,CAAC;AAAA,EACpC;AAAA,EACA,UAAU,MAAM;AACZ,UAAM,SAAS,YAAY,WAAW;AACtC,UAAM,QAAQ,WAAW,OAAO,OAAO,WAAW;AAClD,QAAI,EAAE,YAAY,OAAO,WAAW,KAAK,CAAC;AAAA,EAC9C;AACJ,EAAE;AAEK,SAAS,6BAA6B;AACzC,QAAM,UAAU,uBAAuB,CAAC,MAAM,EAAE,QAAQ;AACxD,QAAM,WAAW,uBAAuB,CAAC,MAAM,EAAE,SAAS;AAE1D,YAAU,MAAM;AACZ,QAAI,CAAC,UAAU;AACX,cAAQ;AAAA,IACZ;AAAA,EACJ,GAAG,CAAC,SAAS,QAAQ,CAAC;AAC1B;","names":[]}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
import { readStorage, writeStorage } from "../utils/browser/safe-storage";
|
|
2
3
|
const STORAGE_KEY = "md-sidebar-expanded";
|
|
3
4
|
function getInitialState() {
|
|
4
|
-
|
|
5
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
6
|
-
if (stored === null) return false;
|
|
7
|
-
return stored === "true";
|
|
5
|
+
return readStorage(STORAGE_KEY) === "true";
|
|
8
6
|
}
|
|
9
7
|
const useMdSidebarStore = create((set) => ({
|
|
10
8
|
isExpanded: getInitialState(),
|
|
11
9
|
toggle: () => set((state) => {
|
|
12
10
|
const next = !state.isExpanded;
|
|
13
|
-
|
|
11
|
+
writeStorage(STORAGE_KEY, String(next));
|
|
14
12
|
return { isExpanded: next };
|
|
15
13
|
}),
|
|
16
14
|
expand: () => {
|
|
17
|
-
|
|
15
|
+
writeStorage(STORAGE_KEY, "true");
|
|
18
16
|
return set({ isExpanded: true });
|
|
19
17
|
},
|
|
20
18
|
collapse: () => {
|
|
21
|
-
|
|
19
|
+
writeStorage(STORAGE_KEY, "false");
|
|
22
20
|
return set({ isExpanded: false });
|
|
23
21
|
}
|
|
24
22
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/store/useMdSidebarStore.ts"],"sourcesContent":["import { create } from \"zustand\";\n\nconst STORAGE_KEY = \"md-sidebar-expanded\";\n\nfunction getInitialState(): boolean {\n
|
|
1
|
+
{"version":3,"sources":["../../src/store/useMdSidebarStore.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { readStorage, writeStorage } from \"../utils/browser/safe-storage\";\n\nconst STORAGE_KEY = \"md-sidebar-expanded\";\n\nfunction getInitialState(): boolean {\n return readStorage(STORAGE_KEY) === \"true\";\n}\n\ntype MdSidebarStore = {\n isExpanded: boolean;\n toggle: () => void;\n expand: () => void;\n collapse: () => void;\n};\n\nexport const useMdSidebarStore = create<MdSidebarStore>((set) => ({\n isExpanded: getInitialState(),\n toggle: () => set((state) => {\n const next = !state.isExpanded;\n writeStorage(STORAGE_KEY, String(next));\n return { isExpanded: next };\n }),\n expand: () => {\n writeStorage(STORAGE_KEY, \"true\");\n return set({ isExpanded: true });\n },\n collapse: () => {\n writeStorage(STORAGE_KEY, \"false\");\n return set({ isExpanded: false });\n },\n}));\n"],"mappings":"AAAA,SAAS,cAAc;AACvB,SAAS,aAAa,oBAAoB;AAE1C,MAAM,cAAc;AAEpB,SAAS,kBAA2B;AAChC,SAAO,YAAY,WAAW,MAAM;AACxC;AASO,MAAM,oBAAoB,OAAuB,CAAC,SAAS;AAAA,EAC9D,YAAY,gBAAgB;AAAA,EAC5B,QAAQ,MAAM,IAAI,CAAC,UAAU;AACzB,UAAM,OAAO,CAAC,MAAM;AACpB,iBAAa,aAAa,OAAO,IAAI,CAAC;AACtC,WAAO,EAAE,YAAY,KAAK;AAAA,EAC9B,CAAC;AAAA,EACD,QAAQ,MAAM;AACV,iBAAa,aAAa,MAAM;AAChC,WAAO,IAAI,EAAE,YAAY,KAAK,CAAC;AAAA,EACnC;AAAA,EACA,UAAU,MAAM;AACZ,iBAAa,aAAa,OAAO;AACjC,WAAO,IAAI,EAAE,YAAY,MAAM,CAAC;AAAA,EACpC;AACJ,EAAE;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function readStorage(key) {
|
|
2
|
+
if (typeof window === "undefined") return null;
|
|
3
|
+
try {
|
|
4
|
+
return window.localStorage?.getItem(key) ?? null;
|
|
5
|
+
} catch {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function writeStorage(key, value) {
|
|
10
|
+
if (typeof window === "undefined") return;
|
|
11
|
+
try {
|
|
12
|
+
window.localStorage?.setItem(key, value);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
readStorage,
|
|
18
|
+
writeStorage
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=safe-storage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/browser/safe-storage.ts"],"sourcesContent":["// Alguns webviews in-app (Google/Meta, Android com storage desabilitado) tanto podem\n// devolver `window.localStorage === null` quanto lançar SecurityError ao simplesmente\n// acessar o getter. `typeof window === 'undefined'` não cobre nenhum dos dois casos.\nexport function readStorage(key: string): string | null {\n if (typeof window === 'undefined') return null;\n try {\n return window.localStorage?.getItem(key) ?? null;\n } catch {\n return null;\n }\n}\n\nexport function writeStorage(key: string, value: string): void {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage?.setItem(key, value);\n } catch {\n // storage indisponível — segue só com o estado em memória\n }\n}\n"],"mappings":"AAGO,SAAS,YAAY,KAA4B;AACtD,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,cAAc,QAAQ,GAAG,KAAK;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,aAAa,KAAa,OAAqB;AAC7D,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,WAAO,cAAc,QAAQ,KAAK,KAAK;AAAA,EACzC,QAAQ;AAAA,EAER;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@ import { ApiError } from "../../../infra/api/types";
|
|
|
3
3
|
import { WhitelabelTokenApiResponse, WhitelabelTokenData } from "../schema";
|
|
4
4
|
import { normalizeHostname } from "../utils/normalize-hostname";
|
|
5
5
|
|
|
6
|
+
const NOT_FOUND_SENTINEL = "__WL_TOKEN_NOT_FOUND__";
|
|
7
|
+
|
|
6
8
|
class WhitelabelService {
|
|
7
9
|
private getApiUrl(): string {
|
|
8
10
|
const apiUrl = process.env.GAPPS_R3_API_URL;
|
|
@@ -56,6 +58,12 @@ class WhitelabelService {
|
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
if (!response.ok) {
|
|
61
|
+
const body = await response.json().catch(() => null);
|
|
62
|
+
|
|
63
|
+
if (response.status === 404 || body?.message === "Whitelabel not found") {
|
|
64
|
+
throw new ApiError("Whitelabel token not found", "TOKEN_NOT_FOUND", 404);
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
console.error("[WhitelabelService] Failed to fetch whitelabel token", {
|
|
60
68
|
response,
|
|
61
69
|
});
|
|
@@ -92,15 +100,31 @@ class WhitelabelService {
|
|
|
92
100
|
});
|
|
93
101
|
|
|
94
102
|
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
103
|
+
if (cachedData.data === NOT_FOUND_SENTINEL) {
|
|
104
|
+
console.log("[WhitelabelService] Negative cache hit for domain", { hostname });
|
|
105
|
+
throw new ApiError("Whitelabel token not found", "TOKEN_NOT_FOUND", 404);
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
console.log("[WhitelabelService] Cache hit for domain", { hostname });
|
|
96
109
|
return JSON.parse(cachedData.data) as WhitelabelTokenData;
|
|
97
110
|
}
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
try {
|
|
113
|
+
const data = await this.fetchFromApi(hostname);
|
|
114
|
+
|
|
115
|
+
await cache.insert(cacheKey, JSON.stringify(data), 604800);
|
|
116
|
+
|
|
117
|
+
return data;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (ApiError.isApiError(error) && error.code === "TOKEN_NOT_FOUND") {
|
|
120
|
+
try {
|
|
121
|
+
await cache.insert(cacheKey, NOT_FOUND_SENTINEL, 60);
|
|
122
|
+
} catch {
|
|
123
|
+
// falha do cache não pode substituir o TOKEN_NOT_FOUND original
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
async getTokenByWhitelabelId(idWl: number): Promise<string> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
+
import { readStorage, writeStorage } from "../utils/browser/safe-storage";
|
|
3
4
|
|
|
4
5
|
const STORAGE_KEY = "desktop-sidebar-expanded";
|
|
5
6
|
|
|
@@ -17,19 +18,19 @@ export const useDesktopSidebarStore = create<DesktopSidebarStore>((set) => ({
|
|
|
17
18
|
_hydrated: false,
|
|
18
19
|
toggle: () => set((state) => {
|
|
19
20
|
const next = !state.isExpanded;
|
|
20
|
-
|
|
21
|
+
writeStorage(STORAGE_KEY, String(next));
|
|
21
22
|
return { isExpanded: next };
|
|
22
23
|
}),
|
|
23
24
|
expand: () => {
|
|
24
|
-
|
|
25
|
+
writeStorage(STORAGE_KEY, "true");
|
|
25
26
|
return set({ isExpanded: true });
|
|
26
27
|
},
|
|
27
28
|
collapse: () => {
|
|
28
|
-
|
|
29
|
+
writeStorage(STORAGE_KEY, "false");
|
|
29
30
|
return set({ isExpanded: false });
|
|
30
31
|
},
|
|
31
32
|
_hydrate: () => {
|
|
32
|
-
const stored =
|
|
33
|
+
const stored = readStorage(STORAGE_KEY);
|
|
33
34
|
const value = stored === null ? true : stored === "true";
|
|
34
35
|
set({ isExpanded: value, _hydrated: true });
|
|
35
36
|
},
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
import { readStorage, writeStorage } from "../utils/browser/safe-storage";
|
|
2
3
|
|
|
3
4
|
const STORAGE_KEY = "md-sidebar-expanded";
|
|
4
5
|
|
|
5
6
|
function getInitialState(): boolean {
|
|
6
|
-
|
|
7
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
8
|
-
if (stored === null) return false;
|
|
9
|
-
return stored === "true";
|
|
7
|
+
return readStorage(STORAGE_KEY) === "true";
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
type MdSidebarStore = {
|
|
@@ -20,15 +18,15 @@ export const useMdSidebarStore = create<MdSidebarStore>((set) => ({
|
|
|
20
18
|
isExpanded: getInitialState(),
|
|
21
19
|
toggle: () => set((state) => {
|
|
22
20
|
const next = !state.isExpanded;
|
|
23
|
-
|
|
21
|
+
writeStorage(STORAGE_KEY, String(next));
|
|
24
22
|
return { isExpanded: next };
|
|
25
23
|
}),
|
|
26
24
|
expand: () => {
|
|
27
|
-
|
|
25
|
+
writeStorage(STORAGE_KEY, "true");
|
|
28
26
|
return set({ isExpanded: true });
|
|
29
27
|
},
|
|
30
28
|
collapse: () => {
|
|
31
|
-
|
|
29
|
+
writeStorage(STORAGE_KEY, "false");
|
|
32
30
|
return set({ isExpanded: false });
|
|
33
31
|
},
|
|
34
32
|
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Alguns webviews in-app (Google/Meta, Android com storage desabilitado) tanto podem
|
|
2
|
+
// devolver `window.localStorage === null` quanto lançar SecurityError ao simplesmente
|
|
3
|
+
// acessar o getter. `typeof window === 'undefined'` não cobre nenhum dos dois casos.
|
|
4
|
+
export function readStorage(key: string): string | null {
|
|
5
|
+
if (typeof window === 'undefined') return null;
|
|
6
|
+
try {
|
|
7
|
+
return window.localStorage?.getItem(key) ?? null;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeStorage(key: string, value: string): void {
|
|
14
|
+
if (typeof window === 'undefined') return;
|
|
15
|
+
try {
|
|
16
|
+
window.localStorage?.setItem(key, value);
|
|
17
|
+
} catch {
|
|
18
|
+
// storage indisponível — segue só com o estado em memória
|
|
19
|
+
}
|
|
20
|
+
}
|