dreame-action/extensions/thank-you-activity/src/activity-api.ts

182 lines
4.1 KiB
TypeScript
Raw Normal View History

2026-06-19 19:35:50 +08:00
export type ActivityResponse =
| {
success: true;
redirectUrl: string;
buttonText: string;
}
| {
success: false;
};
export type CheckoutOrder = {
id?: string;
name?: string;
};
export type CheckoutExtensionApi = {
settings?: {
current?: {
app_url?: unknown;
};
value?: {
app_url?: unknown;
};
};
orderConfirmation?: {
current?: {
order?: CheckoutOrder;
};
value?: {
order?: CheckoutOrder;
};
};
localization?: {
current?: {
language?: {
isoCode?: string;
};
};
value?: {
language?: {
isoCode?: string;
};
};
};
i18n?: {
locale?: string;
};
sessionToken: {
get: () => Promise<string>;
};
};
type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
type RequestActivityCheckOptions = {
appUrl: unknown;
order?: CheckoutOrder;
locale: string;
getSessionToken: () => Promise<string>;
fetchImpl?: FetchLike;
timeoutMs?: number;
};
export const DEFAULT_ACTIVITY_REQUEST_TIMEOUT_MS = 10_000;
export function normalizeAppUrl(rawValue: unknown): string | null {
if (typeof rawValue !== "string" || rawValue.trim() === "") {
return null;
}
try {
const url = new URL(rawValue.trim());
if (url.protocol !== "https:" || url.username || url.password) {
return null;
}
const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "");
return `${url.origin}${pathname}`;
} catch {
return null;
}
}
export async function requestActivityCheck({
appUrl,
order,
locale,
getSessionToken,
fetchImpl = fetch,
timeoutMs = DEFAULT_ACTIVITY_REQUEST_TIMEOUT_MS,
}: RequestActivityCheckOptions): Promise<ActivityResponse> {
const baseUrl = normalizeAppUrl(appUrl);
if (!baseUrl || !order?.id) {
return hiddenActivityResponse();
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const token = await getSessionToken();
if (typeof token !== "string" || token.trim() === "") {
return hiddenActivityResponse();
}
const endpoint = new URL("api/action/activity/check", `${baseUrl}/`).toString();
const response = await fetchImpl(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
orderId: order.id,
orderNumber: order.name,
locale,
}),
cache: "no-store",
signal: controller.signal,
});
if (!response.ok) {
return hiddenActivityResponse();
}
const payload = await response.json().catch(() => null);
return toActivityResponse(payload);
} catch {
return hiddenActivityResponse();
} finally {
clearTimeout(timeout);
}
}
export function getAppUrlSetting(api: CheckoutExtensionApi): unknown {
return api.settings?.current?.app_url ?? api.settings?.value?.app_url;
}
export function getCheckoutOrder(api: CheckoutExtensionApi): CheckoutOrder | undefined {
return api.orderConfirmation?.current?.order ?? api.orderConfirmation?.value?.order;
}
export function getCheckoutLocale(api: CheckoutExtensionApi): string {
return (
api.localization?.current?.language?.isoCode ||
api.localization?.value?.language?.isoCode ||
api.i18n?.locale ||
"en"
);
}
function toActivityResponse(payload: unknown): ActivityResponse {
if (!payload || typeof payload !== "object") {
return hiddenActivityResponse();
}
const response = payload as Record<string, unknown>;
if (response.success !== true) {
return hiddenActivityResponse();
}
if (typeof response.redirectUrl !== "string" || response.redirectUrl.trim() === "") {
return hiddenActivityResponse();
}
if (typeof response.buttonText !== "string" || response.buttonText.trim() === "") {
return hiddenActivityResponse();
}
return {
success: true,
redirectUrl: response.redirectUrl,
buttonText: response.buttonText,
};
}
function hiddenActivityResponse(): ActivityResponse {
return {
success: false,
};
}