-
퍼널로 이동하기
+
+ 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 0000000000000000000000000000000000000000..a8d767ef6a0613b82a66317e1598f6d279452586
GIT binary patch
literal 23605
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
literal 0
HcmV?d00001