diff --git a/src/cloud/api.ts b/src/cloud/api.ts index 77b3834..b6d5189 100644 --- a/src/cloud/api.ts +++ b/src/cloud/api.ts @@ -13,6 +13,10 @@ const f = ( }).then(async (res) => { const text = await res.text(); const resBody = (text ? JSON.parse(text) : undefined) as T; + if (!res.ok) { + const errorBody = resBody as undefined | { message?: string }; + throw new Error(errorBody?.message ?? res.statusText); + } return resBody; }); diff --git a/src/cloud/index.ts b/src/cloud/index.ts index 5d15148..06a87f1 100644 --- a/src/cloud/index.ts +++ b/src/cloud/index.ts @@ -2,11 +2,25 @@ import { startFlow } from "../public-methods"; import { init as flowsInit } from "../init"; import type { FlowsCloudOptions } from "../types"; import { hash } from "../utils"; +import { log } from "../log"; +import { validateFlowsOptions, validateCloudFlowsOptions } from "../validation"; import { api } from "./api"; export * from "../index"; export const init = async (options: FlowsCloudOptions): Promise => { + const cloudValidationResult = validateCloudFlowsOptions(options); + const coreValidationResult = validateFlowsOptions(options); + const validationResult = !cloudValidationResult.valid + ? cloudValidationResult + : coreValidationResult; + if (validationResult.error) + log.error( + `Error validating options at: options.${validationResult.error.path.join(".")} with value:`, + validationResult.error.value, + ); + if (!validationResult.valid) return; + const apiUrl = options.customApiUrl ?? "https://api.flows-cloud.com"; const flows = await api(apiUrl) @@ -15,8 +29,11 @@ export const init = async (options: FlowsCloudOptions): Promise => { userHash: options.userId ? await hash(options.userId) : undefined, }) .catch((err) => { - // eslint-disable-next-line no-console -- useful for debugging - console.error(`Failed to fetch flows from cloud with projectId: ${options.projectId}`, err); + log.error( + `Failed to load data from cloud for %c${options.projectId}%c, make sure projectId is correct and your project domains are correctly set up.\n`, + "font-weight:bold", + err, + ); }); flowsInit({ @@ -28,16 +45,20 @@ export const init = async (options: FlowsCloudOptions): Promise => { const { flowHash, flowId, type, projectId, stepIndex, stepHash, userId } = event; void (async () => - api(apiUrl).sendEvent({ - eventTime: new Date().toISOString(), - flowHash, - flowId, - projectId, - type, - stepHash, - stepIndex: stepIndex?.toString(), - userHash: userId ? await hash(userId) : undefined, - }))(); + api(apiUrl) + .sendEvent({ + eventTime: new Date().toISOString(), + flowHash, + flowId, + projectId, + type, + stepHash, + stepIndex: stepIndex?.toString(), + userHash: userId ? await hash(userId) : undefined, + }) + .catch((err) => { + log.error("Failed to send event to cloud\n", err); + }))(); }, onLocationChange: (pathname, context) => { const params = new URLSearchParams(pathname.split("?")[1] ?? ""); @@ -50,6 +71,9 @@ export const init = async (options: FlowsCloudOptions): Promise => { .then((flow) => { context.addFlowData({ ...flow, draft: true }); startFlow(flow.id, { startDraft: true }); + }) + .catch((err) => { + log.error("Failed to load preview flow\n", err); }); }, }); diff --git a/src/init.ts b/src/init.ts index b3c404e..f9085f1 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,6 +1,7 @@ import { FlowsContext } from "./flows-context"; import { changeWaitMatch, formWaitMatch, locationMatch } from "./form"; import { addHandlers } from "./handlers"; +import { log } from "./log"; import { endFlow, startFlow } from "./public-methods"; import type { FlowsInitOptions } from "./types"; import { validateFlowsOptions } from "./validation"; @@ -15,8 +16,7 @@ export const init = (options: FlowsInitOptions): void => { const _init = (options: FlowsInitOptions): void => { const validationResult = validateFlowsOptions(options); if (validationResult.error) - // eslint-disable-next-line no-console -- useful for user debugging - console.error( + log.error( `Error validating options at: options.${validationResult.error.path.join(".")} with value:`, validationResult.error.value, ); diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..6790005 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,11 @@ +export const log = { + error: (message: string, ...args: unknown[]): void => { + // eslint-disable-next-line no-console -- ignore + console.error( + `%cFlows%c ${message}`, + "color:#fff;background:#ec6441;padding:2px 4px;border-radius:4px", + "", + ...args, + ); + }, +}; diff --git a/src/render.tsx b/src/render.tsx index fa3bc8d..b0bd0fb 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -4,6 +4,7 @@ import type { FlowModalStep, FlowTooltipStep, Placement } from "./types"; import type { FlowState } from "./flow-state"; import { isModalStep, isTooltipStep } from "./utils"; import { Icons } from "./icons"; +import { log } from "./log"; const DISTANCE = 4; const ARROW_SIZE = 6; @@ -60,8 +61,7 @@ const updateTooltip = ({ } }) .catch((err) => { - // eslint-disable-next-line no-console -- Error log - console.warn("Error computing position", err); + log.error("Error computing position\n", err); }); }; diff --git a/src/validation.ts b/src/validation.ts index 1d089fb..e9f0676 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -28,6 +28,7 @@ import type { CompareValue, UserPropertyMatchGroup, FlowsInitOptions, + FlowsCloudOptions, } from "./types"; const WaitOptionsStruct: Describe = object({ @@ -134,7 +135,7 @@ const FlowStruct: Describe = object({ ), }); -const OptionsStruct: Describe = object({ +const OptionsStruct: Describe = type({ flows: optional(array(FlowStruct)), onNextStep: optional(func()) as Describe, onPrevStep: optional(func()) as Describe, @@ -148,6 +149,9 @@ const OptionsStruct: Describe = object({ customApiUrl: optional(string()), onLocationChange: optional(func()) as Describe, }); +const CloudOptionsStruct: Describe> = type({ + customApiUrl: optional(string()), +}); const validateStruct = (struct: Describe) => @@ -164,6 +168,7 @@ const validateStruct = export const isValidFlowsOptions = (options: unknown): options is FlowsOptions => OptionsStruct.is(options); export const validateFlowsOptions = validateStruct(OptionsStruct); +export const validateCloudFlowsOptions = validateStruct(CloudOptionsStruct); export const isValidFlow = (flow: unknown): flow is Flow => FlowStruct.is(flow); export const validateFlow = validateStruct(FlowStruct);