-
퍼널로 이동하기
+
+ Go To Basic Funnel
+ Go To Nested Funnel
+ Go To Guard Funnel
+ Go To Default Step Funnel
);
}
diff --git a/apps/app-router-example/package.json b/apps/app-router-example/package.json
index be83c25..946edf3 100644
--- a/apps/app-router-example/package.json
+++ b/apps/app-router-example/package.json
@@ -11,6 +11,8 @@
"dependencies": {
"@xionhub/funnel-app-router-adapter": "workspace:*",
"@xionhub/funnel-core": "workspace:*",
+ "@xionhub/funnel-client": "workspace:*",
+ "jotai": "^2.9.2",
"next": "14.2.5",
"overlay-kit": "^1.4.1",
"qs": "^6.13.0",
diff --git a/apps/app-router-example/src/basic-funnel.tsx b/apps/app-router-example/src/basic-funnel.tsx
new file mode 100644
index 0000000..5aaa8a1
--- /dev/null
+++ b/apps/app-router-example/src/basic-funnel.tsx
@@ -0,0 +1,59 @@
+"use client";
+import { useFunnel } from "@xionhub/funnel-app-router-adapter";
+import { funnelOptions } from "@xionhub/funnel-core";
+import { useRouter } from "next/navigation";
+
+export const basicFunnelOptions = () =>
+ funnelOptions({
+ steps: ["a", "b", "c"] as const,
+ funnelId: "hello-this-is-funnel-id",
+ });
+
+type Props = {
+ setStep: () => void;
+ step: string;
+};
+
+export const BasicFunnel = () => {
+ const [Funnel, controller] = useFunnel(basicFunnelOptions());
+ const router = useRouter();
+ return (
+
+
+ {
+ router.push(`/funnel?${controller.createStep("b")}`);
+ }}
+ step="a"
+ />
+
+
+ {
+ router.push(`/funnel?${controller.createStep("c")}`);
+ }}
+ step="b"
+ />
+
+
+ {
+ router.push(`/funnel?${controller.createStep("a")}`);
+ }}
+ step="c"
+ />
+
+
+ );
+};
+
+const FunnelItem = ({ setStep, step }: Props) => {
+ return (
+
+
current location {step}
+
+
+ );
+};
diff --git a/apps/app-router-example/src/default-step-funnel.tsx b/apps/app-router-example/src/default-step-funnel.tsx
new file mode 100644
index 0000000..162a082
--- /dev/null
+++ b/apps/app-router-example/src/default-step-funnel.tsx
@@ -0,0 +1,65 @@
+"use client";
+import { useFunnel } from "@xionhub/funnel-app-router-adapter";
+import { funnelOptions } from "@xionhub/funnel-core";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+export const defaultStepFunnelOptions = () =>
+ funnelOptions({
+ steps: ["a", "b", "c"] as const,
+ funnelId: "default-step",
+ });
+
+type Props = {
+ setStep: () => void;
+ step: string;
+};
+
+export const DefaultStepFunnel = () => {
+ const [Funnel, controller] = useFunnel(defaultStepFunnelOptions());
+ const router = useRouter();
+ useEffect(() => {
+ if (controller.step === undefined) {
+ router.replace(`/default-step?${controller.createStep("a")}`);
+ }
+ });
+ return (
+
+
+ {
+ router.push(`/default-step?${controller.createStep("b")}`);
+ }}
+ step="a"
+ />
+
+
+ {
+ router.push(`/default-step?${controller.createStep("c")}`);
+ }}
+ step="b"
+ />
+
+
+ {
+ router.push(`/default-step?${controller.createStep("a")}`);
+ }}
+ step="c"
+ />
+
+
+ );
+};
+
+const FunnelItem = ({ setStep, step }: Props) => {
+ return (
+
+
current location {step}
+
+
+ );
+};
diff --git a/apps/app-router-example/src/example-funnel.ts b/apps/app-router-example/src/example-funnel.ts
deleted file mode 100644
index 37a93a2..0000000
--- a/apps/app-router-example/src/example-funnel.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { FunnelClient, funnelOptions } from "@xionhub/funnel-core";
-
-const EXAMPLE_FUNNEL_ID = "hello-this-is-funnel-id";
-export const exampleFunnelOptions = () =>
- funnelOptions({
- steps: ["a", "b", "c"] as const,
- funnelId: EXAMPLE_FUNNEL_ID,
- });
diff --git a/apps/app-router-example/src/guard-funnel.tsx b/apps/app-router-example/src/guard-funnel.tsx
new file mode 100644
index 0000000..be5a9fd
--- /dev/null
+++ b/apps/app-router-example/src/guard-funnel.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { useFunnel } from "@xionhub/funnel-app-router-adapter";
+import { funnelOptions } from "@xionhub/funnel-core";
+import { useRouter } from "next/navigation";
+import { overlay } from "overlay-kit";
+
+export const guardFunnelOptions = () =>
+ funnelOptions({
+ steps: ["a", "b", "c"] as const,
+ funnelId: "guard-funnel",
+ });
+
+export const GuardFunnel = () => {
+ const [Funnel, controller] = useFunnel(guardFunnelOptions());
+ const router = useRouter();
+ return (
+
+
+ {
+ router.push(`/guard?${controller.createStep("b")}`);
+ }}
+ step="a"
+ />
+
+
+ {
+ if (Math.random() > 0.5) {
+ return true;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ return false;
+ }}
+ onRestrict={async () => {
+ const confirm = await overlay.openAsync(
+ ({ close, isOpen }) =>
+ isOpen && (
+
+
Funnel is restricted
+
+
+
+ ),
+ );
+ if (confirm) {
+ router.push(`/guard?${controller.createStep("a")}`);
+ } else {
+ router.push("/");
+ }
+ }}
+ fallback={<>... funnel validation>}
+ >
+ {
+ router.push(`/guard?${controller.createStep("c")}`);
+ }}
+ step="b"
+ />
+
+
+
+
+ {
+ router.push(`/guard?${controller.createStep("a")}`);
+ }}
+ step="c"
+ />
+
+
+ );
+};
+
+type Props = {
+ setStep: () => void;
+ step: string;
+};
+
+const FunnelItem = ({ setStep, step }: Props) => {
+ return (
+
+
current location {step}
+
+
+ );
+};
diff --git a/apps/app-router-example/src/nested-funnel.tsx b/apps/app-router-example/src/nested-funnel.tsx
new file mode 100644
index 0000000..b5733b0
--- /dev/null
+++ b/apps/app-router-example/src/nested-funnel.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useFunnel } from "@xionhub/funnel-app-router-adapter";
+import { funnelOptions } from "@xionhub/funnel-core";
+import { useRouter, useSearchParams } from "next/navigation";
+
+export const aFunnelOptions = () => funnelOptions({ funnelId: "sadkl", steps: ["astart", "ado", "aend"] as const });
+
+export const bFunnelOptions = () => funnelOptions({ funnelId: "sadkl2", steps: ["bstart", "bdo", "bend"] as const });
+
+export const NestedFunnel = () => {
+ const [AFunnel, aController] = useFunnel(aFunnelOptions());
+ const [BFunnel, bController] = useFunnel(bFunnelOptions());
+ const searchParams = useSearchParams();
+ const router = useRouter();
+
+ return (
+
+
+
+ {
+ router.push(`/nested?${aController.createStep("ado")}`);
+ }}
+ step={"astart"}
+ />
+
+
+ {
+ router.push(
+ `/nested?${bController.createStep("bstart", { searchParams, deleteQueryParams: aController.funnelId })}`,
+ );
+ }}
+ step={"ado"}
+ />
+
+
+ {
+ router.push(`/nested?${aController.createStep("astart")}`);
+ }}
+ step={"aend"}
+ />
+
+
+
+
+
+ {
+ router.push(`/nested?${bController.createStep("bdo")}`);
+ }}
+ step={"bstart"}
+ />
+
+
+ {
+ router.push(`/nested?${bController.createStep("bend")}`);
+ }}
+ step={"bdo"}
+ />
+
+
+ {
+ router.push(
+ `/nested?${aController.createStep("aend", { searchParams, deleteQueryParams: bController.funnelId })}`,
+ );
+ }}
+ step={"bend"}
+ />
+
+
+
+ );
+};
+
+type Props = {
+ setStep: () => void;
+ step: string;
+};
+
+const FunnelItem = ({ setStep, step }: Props) => {
+ return (
+
+
current location {step}
+
+
+ );
+};
diff --git a/biome.json b/biome.json
index 86a5f12..cde4e69 100644
--- a/biome.json
+++ b/biome.json
@@ -6,7 +6,10 @@
"linter": {
"enabled": true,
"rules": {
- "recommended": true
+ "recommended": true,
+ "a11y": {
+ "useButtonType": "off"
+ }
}
},
"formatter": {
diff --git a/packages/funnel-app-router-adapter/package.json b/packages/funnel-app-router-adapter/package.json
index b6ee24c..d375822 100644
--- a/packages/funnel-app-router-adapter/package.json
+++ b/packages/funnel-app-router-adapter/package.json
@@ -25,6 +25,7 @@
"@types/qs": "^6.9.15",
"@types/react": "^18",
"@xionhub/funnel-core": "workspace:*",
+ "@xionhub/funnel-client": "workspace:*",
"next": "^14.2.5",
"qs": "^6.13.0",
"tsup": "^8.1.0",
@@ -32,6 +33,7 @@
},
"peerDependencies": {
"@xionhub/funnel-core": "^0",
+ "@xionhub/funnel-client": "^0",
"next": "^13",
"qs": "^6",
"react": "^16",
diff --git a/packages/funnel-app-router-adapter/src/external/use-funnel-app-router-adapter.tsx b/packages/funnel-app-router-adapter/src/external/use-funnel-app-router-adapter.tsx
index a8259be..3eb5d29 100644
--- a/packages/funnel-app-router-adapter/src/external/use-funnel-app-router-adapter.tsx
+++ b/packages/funnel-app-router-adapter/src/external/use-funnel-app-router-adapter.tsx
@@ -1,53 +1,15 @@
-import {
- type CreateFunnelStepFunction,
- FunnelClient,
- type FunnelOptions,
- type FunnelStepChangeFunction,
- type NonEmptyArray,
- useCoreFunnel,
-} from "@xionhub/funnel-core";
-import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import { FunnelClient } from "@xionhub/funnel-client";
+import { type FunnelOptions, type NonEmptyArray, useCoreFunnel } from "@xionhub/funnel-core";
+import { useSearchParams } from "next/navigation";
export const useFunnelAppRouterAdapter =
>(
options: Omit, "step">,
) => {
const funnelId = options.funnelId;
- const router = useRouter();
const searchParams = useSearchParams();
const queryStep = searchParams.get(funnelId);
const funnelClient = new FunnelClient(options);
- const pathname = usePathname();
const step = (queryStep ?? undefined) as Steps[number] | undefined;
- const [Funnel, controller] = useCoreFunnel({ ...options, step });
-
- const createFunnelStep: CreateFunnelStepFunction = (step, options) => {
- if (!options?.searchParams) {
- return { [funnelId]: step };
- }
- const allQueryString = funnelClient.getQueryString(options?.searchParams);
-
- const deleteKeyList = Array.isArray(options?.deleteQueryParams)
- ? options?.deleteQueryParams
- : ([options?.deleteQueryParams].filter(Boolean) as string[]);
-
- const stepObject = funnelClient.createStepObject(step, funnelClient.deleteStep(allQueryString, deleteKeyList));
- return stepObject;
- };
-
- const onStepChange: FunnelStepChangeFunction = (newStep, options) => {
- const stepObject = createFunnelStep(newStep, { ...options, searchParams });
- const newUrl = `${pathname}${funnelClient.stringifyStep(stepObject)}`;
-
- if (options?.type === "replace") {
- return router.replace(newUrl);
- }
-
- if (options?.type === "back") {
- return router.back();
- }
-
- return router.push(newUrl);
- };
-
- return [Funnel, { ...controller, onStepChange, createFunnelStep }] as const;
+ const [Funnel, { onStepChange, ...controller }] = useCoreFunnel({ ...options, step });
+ return [Funnel, { ...controller, createStep: funnelClient.createStep }] as const;
};
diff --git a/packages/funnel-client/package.json b/packages/funnel-client/package.json
new file mode 100644
index 0000000..3314d5e
--- /dev/null
+++ b/packages/funnel-client/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@xionhub/funnel-client",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "require": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ }
+ },
+ "scripts": {
+ "build": "tsup"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "devDependencies": {
+ "@types/qs": "^6.9.15",
+ "@types/react": "^18",
+ "qs": "^6.13.0",
+ "tsup": "^8.1.0",
+ "typescript": "latest",
+ "@xionhub/funnel-core": "workspace:*"
+ },
+ "peerDependencies": {
+ "react": "^16",
+ "react-dom": "^16",
+ "qs": "^6",
+ "@xionhub/funnel-core": "^0"
+ }
+}
diff --git a/packages/funnel-client/src/external/funnel-client.ts b/packages/funnel-client/src/external/funnel-client.ts
new file mode 100644
index 0000000..bcac342
--- /dev/null
+++ b/packages/funnel-client/src/external/funnel-client.ts
@@ -0,0 +1,59 @@
+import type { FunnelOptions, NonEmptyArray } from "@xionhub/funnel-core";
+import type QueryString from "qs";
+import qs from "qs";
+
+type CreateStepOptionsType = {
+ searchParams?: URLSearchParams;
+ deleteQueryParams?: string[] | string;
+ qsOptions?: QueryString.IStringifyBaseOptions;
+};
+
+export class FunnelClient> {
+ funnelId: string;
+ steps: T;
+
+ constructor(props: FunnelOptions) {
+ this.funnelId = props.funnelId;
+ this.steps = props.steps;
+ }
+
+ createStep(step: T[number], options?: CreateStepOptionsType) {
+ const { searchParams, deleteQueryParams, qsOptions } = options ?? {};
+ const deleteList = (
+ Array.isArray(deleteQueryParams) ? deleteQueryParams : [deleteQueryParams].filter(Boolean)
+ ) as string[];
+ const searchParamToObj = this.getQueryString(searchParams ?? new URLSearchParams());
+ return this.stringifyStep(this.deleteStep(this.createStepObject(step, searchParamToObj), deleteList), qsOptions);
+ }
+
+ getQueryString>(searchParams: URLSearchParams) {
+ const result = {} as Record;
+ searchParams.forEach((value, key) => {
+ result[key] = value;
+ });
+ return result as T;
+ }
+
+ createStepObject(value: T[number], context?: Record) {
+ return { ...context, [this.funnelId]: value } as const;
+ }
+
+ // biome-ignore lint/suspicious/noExplicitAny:
+ deleteStep, K extends keyof T>(obj: T, keys: K[]): Omit {
+ const result = { ...obj };
+
+ for (const key of keys) {
+ delete result[key];
+ }
+
+ return result as Omit;
+ }
+
+ stringifyStep(context: Record, options?: QueryString.IStringifyBaseOptions) {
+ return qs.stringify(context, options);
+ }
+
+ parseQueryString(queryString: string, options?: QueryString.IStringifyBaseOptions) {
+ return qs.parse(queryString, options) as T;
+ }
+}
diff --git a/packages/funnel-client/src/index.ts b/packages/funnel-client/src/index.ts
new file mode 100644
index 0000000..c5fd954
--- /dev/null
+++ b/packages/funnel-client/src/index.ts
@@ -0,0 +1,2 @@
+import { FunnelClient } from "./external/funnel-client";
+export { FunnelClient };
diff --git a/packages/funnel-client/tsconfig.json b/packages/funnel-client/tsconfig.json
new file mode 100644
index 0000000..244bc1d
--- /dev/null
+++ b/packages/funnel-client/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/funnel-client/tsup.config.ts b/packages/funnel-client/tsup.config.ts
new file mode 100644
index 0000000..3e69dbd
--- /dev/null
+++ b/packages/funnel-client/tsup.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ format: ["cjs", "esm"],
+ entry: ["./src/index.ts"],
+ sourcemap: true,
+ dts: true,
+ clean: true,
+ minify: true,
+ treeshake: true,
+});
diff --git a/packages/funnel-core/package.json b/packages/funnel-core/package.json
index 78eac72..cbef53e 100644
--- a/packages/funnel-core/package.json
+++ b/packages/funnel-core/package.json
@@ -22,15 +22,12 @@
"access": "public"
},
"devDependencies": {
- "@types/qs": "^6.9.15",
"@types/react": "^18",
- "qs": "^6.13.0",
"tsup": "^8.1.0",
"typescript": "latest"
},
"peerDependencies": {
"react": "^16",
- "react-dom": "^16",
- "qs": "^6"
+ "react-dom": "^16"
}
}
diff --git a/packages/funnel-core/src/external/external-utils.ts b/packages/funnel-core/src/external/external-utils.ts
index c2bfa6d..1959cfd 100644
--- a/packages/funnel-core/src/external/external-utils.ts
+++ b/packages/funnel-core/src/external/external-utils.ts
@@ -1,49 +1,3 @@
-import qs from "qs";
import type { FunnelOptions, NonEmptyArray } from "./types";
export const funnelOptions = >(props: FunnelOptions): FunnelOptions => props;
-
-export class FunnelClient> {
- funnelId: string;
- steps: T;
-
- constructor(props: FunnelOptions) {
- this.funnelId = props.funnelId;
- this.steps = props.steps;
- }
-
- createStep(value: T[number], context?: Record) {
- return this.stringifyStep(this.createStepObject(value, context));
- }
-
- getQueryString>(searchParams: URLSearchParams) {
- const result = {} as Record;
- searchParams.forEach((value, key) => {
- result[key] = value;
- });
- return result as T;
- }
-
- createStepObject(value: T[number], context?: Record) {
- return { ...context, [this.funnelId]: value } as const;
- }
-
- // biome-ignore lint/suspicious/noExplicitAny:
- deleteStep, K extends keyof T>(obj: T, keys: K[]): Omit {
- const result = { ...obj };
-
- for (const key of keys) {
- delete result[key];
- }
-
- return result as Omit;
- }
-
- stringifyStep(context: Record) {
- return qs.stringify(context, { addQueryPrefix: true });
- }
-
- parseQueryString(queryString: string) {
- return qs.parse(queryString, { ignoreQueryPrefix: true }) as T;
- }
-}
diff --git a/packages/funnel-core/src/external/guard.tsx b/packages/funnel-core/src/external/guard.tsx
index 31acb45..63dcfa7 100644
--- a/packages/funnel-core/src/external/guard.tsx
+++ b/packages/funnel-core/src/external/guard.tsx
@@ -1,44 +1,34 @@
"use client";
-import { useEffect, useRef, useState } from "react";
+import { useRef, useState } from "react";
+import { useIsomorphicLayoutEffect } from "../internal/use-isomorphic-layout-effect";
import type { GuardProps } from "./types";
-export const Guard = ({ condition, children, fallback, onRestrict }: GuardProps) => {
+export const Guard = (props: GuardProps) => {
+ const { condition, onRestrict, fallback, children } = props;
const [isRender, setIsRender] = useState(false);
const isOnce = useRef(true);
- const canImmediateRender =
- (typeof condition === "boolean" && condition) ||
- (typeof condition === "function" && typeof condition() === "boolean" && condition());
- useEffect(() => {
- let result: boolean;
- const check = async () => {
- if (canImmediateRender) {
- return () => {};
+ useIsomorphicLayoutEffect(() => {
+ if (!isOnce.current) {
+ return () => {};
+ }
+ const callCondition = async () => {
+ isOnce.current = false;
+ const result = typeof condition === "function" ? await condition() : condition;
+ const byResult = props?.conditionBy ? props?.conditionBy?.(result as Awaited) : result;
+ if (typeof byResult !== "boolean") {
+ throw new Error("condition should be boolean");
}
-
- if (typeof condition === "function") {
- result = await condition();
- if (result === false) {
- onRestrict?.();
- } else {
- setIsRender(true);
- }
+ if (byResult) {
+ setIsRender(true);
}
- if (typeof condition === "boolean") {
- if (condition === false) {
- onRestrict?.();
- } else {
- setIsRender(true);
- }
+ if (!byResult) {
+ onRestrict?.(result as Awaited);
}
};
+ callCondition();
+ }, []);
- if (isOnce.current) {
- check();
- isOnce.current = false;
- }
- }, [canImmediateRender, condition, onRestrict]);
-
- return canImmediateRender || isRender ? children : fallback;
+ return isRender ? children : fallback;
};
diff --git a/packages/funnel-core/src/external/types.ts b/packages/funnel-core/src/external/types.ts
index 759bdec..0c691e3 100644
--- a/packages/funnel-core/src/external/types.ts
+++ b/packages/funnel-core/src/external/types.ts
@@ -4,12 +4,7 @@ export type NonEmptyArray = readonly [T, ...T[]];
export type RoutesEventType = "replace" | "push" | "back";
-export type DeleteQueryParams = { deleteQueryParams?: string[] | string };
-
-export type FunnelStepChangeFunction> = (
- step: T[number],
- options?: { type?: RoutesEventType } & DeleteQueryParams,
-) => void;
+export type FunnelStepChangeFunction> = (step: T[number]) => void;
export type RouteFunnelProps> = Omit, "steps" | "step">;
@@ -24,22 +19,23 @@ export interface StepProps> {
children: React.ReactNode;
}
-export interface GuardProps {
- condition: boolean | (() => boolean | Promise);
+export type GuardProps = {
+ condition: (() => T) | (() => Promise) | boolean | (() => boolean) | (() => Promise);
children?: ReactNode;
- onRestrict?: () => void;
+ onRestrict?: (param: Awaited) => void;
+ conditionBy?: (param: Awaited) => boolean;
fallback?: ReactNode;
-}
+};
export type CreateFunnelStepFunction> = (
step: Steps[number],
options?: { deleteQueryParams?: string[] | string; searchParams?: URLSearchParams },
) => Record;
-export type FunnelAdapterReturnType> = [
+export type FunnelAdapterReturnType, T = unknown> = [
((props: RouteFunnelProps) => JSX.Element) & {
Step: (props: StepProps) => JSX.Element;
- Guard: (props: GuardProps) => JSX.Element;
+ Guard: (props: GuardProps) => JSX.Element;
},
{
funnelId: string;
diff --git a/packages/funnel-core/src/external/use-core-funnel.tsx b/packages/funnel-core/src/external/use-core-funnel.tsx
index 7d63193..bc612cd 100644
--- a/packages/funnel-core/src/external/use-core-funnel.tsx
+++ b/packages/funnel-core/src/external/use-core-funnel.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from "react";
+import { useMemo } from "react";
import { useDraft } from "../internal/use-draft";
import { Funnel } from "./funnel";
import { Guard } from "./guard";
@@ -16,11 +16,12 @@ export const useCoreFunnel = >(
options: FunnelOptions & { onStepChange?: FunnelStepChangeFunction },
) => {
const [_step, _setStep] = useDraft(options?.step);
+
const steps = options.steps;
- const step = options?.step;
- const funnelId = options?.funnelId;
+ const step = options?.step ?? _step;
+ const funnelId = options.funnelId;
- const _onStepChange: FunnelStepChangeFunction = (param: Steps[number], routeOptions) => {
+ const _onStepChange: FunnelStepChangeFunction = (param: Steps[number]) => {
_setStep(param);
};
@@ -35,12 +36,12 @@ export const useCoreFunnel = >(
Step: (props: StepProps) => {
return ;
},
- Guard: (props: GuardProps) => {
+ Guard: (props: GuardProps) => {
return ;
},
},
);
}, [step, steps]);
- return [FunnelComponent, { funnelId, step, onStepChange }] as const;
+ return [FunnelComponent, { funnelId, step, onStepChange, steps }] as const;
};
diff --git a/packages/funnel-core/src/index.ts b/packages/funnel-core/src/index.ts
index 8dc0875..5766af2 100644
--- a/packages/funnel-core/src/index.ts
+++ b/packages/funnel-core/src/index.ts
@@ -1,4 +1,4 @@
-import { FunnelClient, funnelOptions } from "./external/external-utils";
+import { funnelOptions } from "./external/external-utils";
import { Guard as FunnelGuard } from "./external/guard";
import type {
CreateFunnelStepFunction,
@@ -14,7 +14,7 @@ import type {
} from "./external/types";
import { useCoreFunnel } from "./external/use-core-funnel";
-export { useCoreFunnel, FunnelGuard, funnelOptions, FunnelClient };
+export { useCoreFunnel, FunnelGuard, funnelOptions };
export type {
NonEmptyArray,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d65e107..54b598d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,9 +35,15 @@ importers:
'@xionhub/funnel-app-router-adapter':
specifier: workspace:*
version: link:../../packages/funnel-app-router-adapter
+ '@xionhub/funnel-client':
+ specifier: workspace:*
+ version: link:../../packages/funnel-client
'@xionhub/funnel-core':
specifier: workspace:*
version: link:../../packages/funnel-core
+ jotai:
+ specifier: ^2.9.2
+ version: 2.9.2(@types/react@18.3.3)(react@18.3.1)
next:
specifier: 14.2.5
version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -91,6 +97,9 @@ importers:
'@types/react':
specifier: ^18
version: 18.3.3
+ '@xionhub/funnel-client':
+ specifier: workspace:*
+ version: link:../funnel-client
'@xionhub/funnel-core':
specifier: workspace:*
version: link:../funnel-core
@@ -107,7 +116,7 @@ importers:
specifier: latest
version: 5.5.4
- packages/funnel-core:
+ packages/funnel-client:
dependencies:
react:
specifier: ^16
@@ -122,6 +131,9 @@ importers:
'@types/react':
specifier: ^18
version: 18.3.3
+ '@xionhub/funnel-core':
+ specifier: workspace:*
+ version: link:../funnel-core
qs:
specifier: ^6.13.0
version: 6.13.0
@@ -132,6 +144,25 @@ importers:
specifier: latest
version: 5.5.4
+ packages/funnel-core:
+ dependencies:
+ react:
+ specifier: ^16
+ version: 16.14.0
+ react-dom:
+ specifier: ^16
+ version: 16.14.0(react@16.14.0)
+ devDependencies:
+ '@types/react':
+ specifier: ^18
+ version: 18.3.3
+ tsup:
+ specifier: ^8.1.0
+ version: 8.2.4(jiti@1.21.6)(postcss@8.4.40)(typescript@5.5.4)(yaml@2.5.0)
+ typescript:
+ specifier: latest
+ version: 5.5.4
+
packages:
'@aashutoshrathi/word-wrap@1.2.6':
@@ -1575,6 +1606,18 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
+ jotai@2.9.2:
+ resolution: {integrity: sha512-jIBXEadOHCziOuMY6HAy2KQcHipGhnsbF+twqh8Lcmcz/Yei0gdBtW5mOYdKmbQxGqkvfvXM3w/oHtJ2WNGSFg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=17.0.0'
+ react: '>=17.0.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@@ -4134,6 +4177,11 @@ snapshots:
jiti@1.21.6: {}
+ jotai@2.9.2(@types/react@18.3.3)(react@18.3.1):
+ optionalDependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+
joycon@3.1.1: {}
js-tokens@4.0.0: {}
diff --git a/thumbnail.png b/thumbnail.png
new file mode 100644
index 0000000..a8d767e
Binary files /dev/null and b/thumbnail.png differ