@founderhq/journeys 0.3.67 → 0.4.1

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/index.cjs CHANGED
@@ -676,6 +676,8 @@ function JourneyProvider({
676
676
  const { trigger } = react$1.useWebHaptics();
677
677
  const onEventRef = React.useRef(onEvent);
678
678
  const rawAnswersRef = React.useRef({});
679
+ const sessionStartEmittedRef = React.useRef(false);
680
+ const lastViewedStepIdRef = React.useRef(null);
679
681
  const [discountCodeDialog, setDiscountCodeDialog] = React.useState(null);
680
682
  React.useEffect(() => {
681
683
  onEventRef.current = onEvent;
@@ -735,6 +737,39 @@ function JourneyProvider({
735
737
  () => normalizedConfig.steps.flatMap(getStepVariables),
736
738
  [normalizedConfig.steps]
737
739
  );
740
+ React.useEffect(() => {
741
+ var _a, _b;
742
+ const currentStep = config.steps[currentStepIndex];
743
+ if (!currentStep) return;
744
+ const eventAnswers = getEventAnswers(
745
+ config.computedVariables,
746
+ rawAnswersRef.current
747
+ );
748
+ const strippedStep = stripOptionsFromEventStep(currentStep);
749
+ if (!sessionStartEmittedRef.current) {
750
+ sessionStartEmittedRef.current = true;
751
+ (_a = onEventRef.current) == null ? void 0 : _a.call(onEventRef, __spreadProps(__spreadValues({
752
+ type: "session_start",
753
+ step: strippedStep
754
+ }, eventAnswers), {
755
+ variables: allVariables
756
+ }));
757
+ }
758
+ if (lastViewedStepIdRef.current !== currentStep.id) {
759
+ lastViewedStepIdRef.current = currentStep.id;
760
+ (_b = onEventRef.current) == null ? void 0 : _b.call(onEventRef, __spreadProps(__spreadValues({
761
+ type: "step_view",
762
+ step: strippedStep
763
+ }, eventAnswers), {
764
+ variables: allVariables
765
+ }));
766
+ }
767
+ }, [
768
+ allVariables,
769
+ config.computedVariables,
770
+ config.steps,
771
+ currentStepIndex
772
+ ]);
738
773
  const setAnswer = React.useCallback((stepId, answer) => {
739
774
  if (computedVariableIds.has(stepId)) return;
740
775
  setRawAnswers((prev) => {
@@ -2663,9 +2698,11 @@ function ButtonBlock({
2663
2698
  onGoToStep,
2664
2699
  onPurchase,
2665
2700
  onOpenDiscountCode,
2666
- disabled
2701
+ disabled,
2702
+ isLastStep
2667
2703
  }) {
2668
2704
  const handleClick = () => {
2705
+ var _a;
2669
2706
  if (!action) {
2670
2707
  onNext == null ? void 0 : onNext();
2671
2708
  return;
@@ -2685,6 +2722,9 @@ function ButtonBlock({
2685
2722
  }
2686
2723
  break;
2687
2724
  case "link":
2725
+ if ((_a = action.completeBeforeNavigate) != null ? _a : isLastStep) {
2726
+ onNext == null ? void 0 : onNext();
2727
+ }
2688
2728
  if (action.external) {
2689
2729
  window.open(action.url, "_blank", "noopener,noreferrer");
2690
2730
  } else {
@@ -4175,7 +4215,8 @@ function ColumnsBlock({
4175
4215
  answers,
4176
4216
  onNext,
4177
4217
  onBack,
4178
- onGoToStep
4218
+ onGoToStep,
4219
+ isLastStep
4179
4220
  }) {
4180
4221
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "jy-columns-container", children: /* @__PURE__ */ jsxRuntime.jsx(
4181
4222
  "div",
@@ -4201,6 +4242,7 @@ function ColumnsBlock({
4201
4242
  onBack: onBack != null ? onBack : (() => {
4202
4243
  }),
4203
4244
  onGoToStep,
4245
+ isLastStep,
4204
4246
  wrapInScrollViewport: false
4205
4247
  }
4206
4248
  )
@@ -7876,7 +7918,11 @@ function HiddenBlockSlot({ block }) {
7876
7918
  return /* @__PURE__ */ jsxRuntime.jsx(
7877
7919
  "div",
7878
7920
  {
7879
- className: cn(widthClass, resolvedMaxWidth ? "mx-auto" : void 0, block.className),
7921
+ className: cn(
7922
+ widthClass,
7923
+ resolvedMaxWidth ? "mx-auto" : void 0,
7924
+ block.className
7925
+ ),
7880
7926
  style: __spreadProps(__spreadValues(__spreadValues({}, resolvedMaxWidth ? { maxWidth: resolvedMaxWidth } : null), block.style), {
7881
7927
  visibility: "hidden"
7882
7928
  }),
@@ -8059,7 +8105,7 @@ function resolveAppliedDiscountForDialog(answers, discountVariable, planId) {
8059
8105
  if (!planId) return answer.planId ? void 0 : answer;
8060
8106
  return discountAppliesToPlan(answer, planId) ? answer : void 0;
8061
8107
  }
8062
- function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToStep, onPurchase, onOpenDiscountCode, inputsValid) {
8108
+ function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToStep, onPurchase, onOpenDiscountCode, inputsValid, isLastStep) {
8063
8109
  var _a, _b;
8064
8110
  const conditionMet = !block.condition || evaluateCondition(block.condition, answers);
8065
8111
  const hasExitAnim = ((_a = block.exitAnimation) == null ? void 0 : _a.preset) && block.exitAnimation.preset !== "none";
@@ -8086,8 +8132,9 @@ function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToS
8086
8132
  onGoToStep,
8087
8133
  onPurchase,
8088
8134
  onOpenDiscountCode,
8089
- disabled: buttonDisabledForValidity(resolvedProps, inputsValid)
8090
- }) : block.type === "columns" ? __spreadProps(__spreadValues({}, resolvedProps), { answers, onNext, onBack, onGoToStep }) : block.type === "gravity_bin" ? __spreadProps(__spreadValues({}, resolvedProps), { answers }) : resolvedProps;
8135
+ disabled: buttonDisabledForValidity(resolvedProps, inputsValid),
8136
+ isLastStep
8137
+ }) : block.type === "columns" ? __spreadProps(__spreadValues({}, resolvedProps), { answers, onNext, onBack, onGoToStep, isLastStep }) : block.type === "gravity_bin" ? __spreadProps(__spreadValues({}, resolvedProps), { answers }) : resolvedProps;
8091
8138
  const idx = visibleIndexRef.current;
8092
8139
  visibleIndexRef.current++;
8093
8140
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -8109,6 +8156,7 @@ function BlockRenderer({
8109
8156
  onNext,
8110
8157
  onBack,
8111
8158
  onGoToStep,
8159
+ isLastStep,
8112
8160
  scrollViewportRef,
8113
8161
  wrapInScrollViewport = true
8114
8162
  }) {
@@ -8194,7 +8242,8 @@ function BlockRenderer({
8194
8242
  onGoToStep,
8195
8243
  onPurchase,
8196
8244
  onOpenDiscountCode,
8197
- inputsValid
8245
+ inputsValid,
8246
+ isLastStep
8198
8247
  );
8199
8248
  };
8200
8249
  if (!wrapInScrollViewport && stickyBlocks.length === 0) {
@@ -8217,7 +8266,11 @@ function BlockRenderer({
8217
8266
  "div",
8218
8267
  {
8219
8268
  className: layoutClasses(layout),
8220
- style: { gap: `${gap}rem`, maxWidth: layout == null ? void 0 : layout.maxWidth, width: "100%" },
8269
+ style: {
8270
+ gap: `${gap}rem`,
8271
+ maxWidth: layout == null ? void 0 : layout.maxWidth,
8272
+ width: "100%"
8273
+ },
8221
8274
  children: contentBlocks.map((block, i) => renderOne(block, i))
8222
8275
  }
8223
8276
  )
@@ -8228,7 +8281,11 @@ function BlockRenderer({
8228
8281
  ) }) : null
8229
8282
  ] });
8230
8283
  }
8231
- function InfoPageStep({ config, onNext }) {
8284
+ function InfoPageStep({
8285
+ config,
8286
+ onNext,
8287
+ isLastStep
8288
+ }) {
8232
8289
  var _a;
8233
8290
  const { answers } = useJourneyState();
8234
8291
  const { goBack, goToStep } = useJourneyActions();
@@ -8236,9 +8293,7 @@ function InfoPageStep({ config, onNext }) {
8236
8293
  const sortedTimelineEvents = React.useMemo(
8237
8294
  () => {
8238
8295
  var _a2, _b;
8239
- return [...(_b = (_a2 = config.scrollTimeline) == null ? void 0 : _a2.events) != null ? _b : []].sort(
8240
- (a, b) => a.at - b.at
8241
- );
8296
+ return [...(_b = (_a2 = config.scrollTimeline) == null ? void 0 : _a2.events) != null ? _b : []].sort((a, b) => a.at - b.at);
8242
8297
  },
8243
8298
  [(_a = config.scrollTimeline) == null ? void 0 : _a.events]
8244
8299
  );
@@ -8418,6 +8473,7 @@ function InfoPageStep({ config, onNext }) {
8418
8473
  onNext,
8419
8474
  onBack: goBack,
8420
8475
  onGoToStep: goToStep,
8476
+ isLastStep,
8421
8477
  scrollViewportRef
8422
8478
  }
8423
8479
  );
@@ -9285,7 +9341,7 @@ var STEP_REGISTRY = {
9285
9341
  function StepRenderer({ config }) {
9286
9342
  var _a, _b;
9287
9343
  const { answers } = useJourneyState();
9288
- const { setAnswer, goNext } = useJourneyActions();
9344
+ const { setAnswer, goNext, isLastStep } = useJourneyActions();
9289
9345
  const resolvedConfig = React.useMemo(() => {
9290
9346
  var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k;
9291
9347
  const hasTemplates = ((_a2 = config.preface) == null ? void 0 : _a2.includes("${")) || ((_b2 = config.question) == null ? void 0 : _b2.includes("${")) || ((_c = config.description) == null ? void 0 : _c.includes("${")) || ((_d = config.buttonText) == null ? void 0 : _d.includes("${")) || ((_e = config.footerText) == null ? void 0 : _e.includes("${")) || ((_g = (_f = config.swipeLabels) == null ? void 0 : _f.yes) == null ? void 0 : _g.includes("${")) || ((_i = (_h = config.swipeLabels) == null ? void 0 : _h.no) == null ? void 0 : _i.includes("${")) || ((_j = config.swipeCards) != null ? _j : []).some(
@@ -9338,7 +9394,8 @@ function StepRenderer({ config }) {
9338
9394
  }
9339
9395
  setAnswer(answerKey, answer);
9340
9396
  },
9341
- onNext: () => goNext()
9397
+ onNext: () => goNext(),
9398
+ isLastStep: isLastStep()
9342
9399
  }
9343
9400
  );
9344
9401
  }
@@ -9635,19 +9692,456 @@ function JourneyShell({ className, theme } = {}) {
9635
9692
  ) })
9636
9693
  ] });
9637
9694
  }
9695
+ var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
9696
+ var JOURNEY_LIBRARY_VERSION = "0.4.1";
9697
+ function randomId(prefix) {
9698
+ const cryptoRef = globalThis.crypto;
9699
+ if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
9700
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
9701
+ }
9702
+ function readJson(storage, key, fallback) {
9703
+ try {
9704
+ const raw = storage.getItem(key);
9705
+ return raw ? JSON.parse(raw) : fallback;
9706
+ } catch (e) {
9707
+ return fallback;
9708
+ }
9709
+ }
9710
+ function getStoredId(storage, key, prefix) {
9711
+ try {
9712
+ const existing = storage.getItem(key);
9713
+ if (existing) return existing;
9714
+ const next = randomId(prefix);
9715
+ storage.setItem(key, next);
9716
+ return next;
9717
+ } catch (e) {
9718
+ return randomId(prefix);
9719
+ }
9720
+ }
9721
+ function isRecord(value) {
9722
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9723
+ }
9724
+ function cleanObject(value) {
9725
+ if (!value || typeof value !== "object") return {};
9726
+ return __spreadValues({}, value);
9727
+ }
9728
+ var DEFAULT_REDACTED_URL_PARAMS = [
9729
+ "access_token",
9730
+ "api_key",
9731
+ "auth",
9732
+ "code",
9733
+ "email",
9734
+ "key",
9735
+ "otp",
9736
+ "password",
9737
+ "phone",
9738
+ "secret",
9739
+ "signature",
9740
+ "token"
9741
+ ];
9742
+ function safeContextFactory(factory) {
9743
+ if (typeof factory !== "function") return void 0;
9744
+ try {
9745
+ const value = factory();
9746
+ return isRecord(value) ? value : void 0;
9747
+ } catch (e) {
9748
+ return void 0;
9749
+ }
9750
+ }
9751
+ function safeTimezone() {
9752
+ try {
9753
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
9754
+ } catch (e) {
9755
+ return void 0;
9756
+ }
9757
+ }
9758
+ function campaignContext(params) {
9759
+ var _a, _b, _c, _d, _e;
9760
+ return cleanObject({
9761
+ source: (_a = params.get("utm_source")) != null ? _a : void 0,
9762
+ medium: (_b = params.get("utm_medium")) != null ? _b : void 0,
9763
+ name: (_c = params.get("utm_campaign")) != null ? _c : void 0,
9764
+ term: (_d = params.get("utm_term")) != null ? _d : void 0,
9765
+ content: (_e = params.get("utm_content")) != null ? _e : void 0
9766
+ });
9767
+ }
9768
+ function redactedSearch(params, redactUrlParams) {
9769
+ if ([...params.keys()].length === 0) return "";
9770
+ const next = new URLSearchParams(params);
9771
+ const redactAll = redactUrlParams === true;
9772
+ const names = Array.isArray(redactUrlParams) && redactUrlParams.length > 0 ? redactUrlParams : DEFAULT_REDACTED_URL_PARAMS;
9773
+ const redactedNames = new Set(names.map((name) => name.toLowerCase()));
9774
+ for (const key of [...next.keys()]) {
9775
+ if (redactAll || redactedNames.has(key.toLowerCase())) {
9776
+ next.set(key, "[redacted]");
9777
+ }
9778
+ }
9779
+ const value = next.toString();
9780
+ return value ? `?${value}` : "";
9781
+ }
9782
+ function pageContext(redactUrlParams) {
9783
+ if (typeof window === "undefined") return void 0;
9784
+ const params = new URLSearchParams(window.location.search);
9785
+ const campaign = campaignContext(params);
9786
+ const search = redactedSearch(params, redactUrlParams);
9787
+ return cleanObject({
9788
+ url: `${window.location.origin}${window.location.pathname}${search}${window.location.hash}`,
9789
+ path: window.location.pathname,
9790
+ search: search || void 0,
9791
+ title: document.title,
9792
+ referrer: document.referrer,
9793
+ campaign
9794
+ });
9795
+ }
9796
+ function captureContext(capture, runtime) {
9797
+ var _a, _b;
9798
+ const customContext = cleanObject(__spreadValues(__spreadValues({}, (_a = capture.context) != null ? _a : {}), (_b = safeContextFactory(capture.contextFactory)) != null ? _b : {}));
9799
+ if (capture.captureContext === false) {
9800
+ return customContext;
9801
+ }
9802
+ const page = pageContext(capture.redactUrlParams);
9803
+ const navigatorRef = typeof window !== "undefined" ? window.navigator : void 0;
9804
+ const screenRef = typeof window !== "undefined" ? window.screen : void 0;
9805
+ return cleanObject(__spreadValues({
9806
+ source: { type: "embed", product: "journeys" },
9807
+ library: {
9808
+ name: JOURNEY_LIBRARY_NAME,
9809
+ version: JOURNEY_LIBRARY_VERSION
9810
+ },
9811
+ anonymousId: runtime.visitorId,
9812
+ sessionId: runtime.clientSessionId,
9813
+ locale: navigatorRef == null ? void 0 : navigatorRef.language,
9814
+ timezone: safeTimezone(),
9815
+ userAgent: navigatorRef == null ? void 0 : navigatorRef.userAgent,
9816
+ page,
9817
+ campaign: isRecord(page) ? page.campaign : void 0,
9818
+ screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
9819
+ }, customContext));
9820
+ }
9821
+ function toJsonRecord(value) {
9822
+ return isRecord(value) ? value : {};
9823
+ }
9824
+ function answerValue(answers, variable) {
9825
+ return Object.prototype.hasOwnProperty.call(answers, variable) ? answers[variable] : void 0;
9826
+ }
9827
+ function stepAnswerVariables(step) {
9828
+ var _a, _b, _c, _d, _e;
9829
+ if (step.type === "swipe_cards") {
9830
+ return ((_a = step.swipeCards) != null ? _a : []).map((card) => ({
9831
+ stepId: step.id,
9832
+ variable: card.variable,
9833
+ label: card.text
9834
+ }));
9835
+ }
9836
+ if (step.type === "input") {
9837
+ return ((_b = step.fields) != null ? _b : []).map((field) => {
9838
+ var _a2;
9839
+ return {
9840
+ stepId: step.id,
9841
+ variable: (_a2 = field.variable) != null ? _a2 : field.id,
9842
+ label: field.label,
9843
+ fieldType: field.type
9844
+ };
9845
+ });
9846
+ }
9847
+ return [
9848
+ {
9849
+ stepId: step.id,
9850
+ variable: (_c = step.variable) != null ? _c : step.id,
9851
+ label: (_e = (_d = step.question) != null ? _d : step.preface) != null ? _e : step.id
9852
+ }
9853
+ ];
9854
+ }
9855
+ function buildAnswerSnapshot(config, answers) {
9856
+ const items = [];
9857
+ for (const step of config.steps) {
9858
+ for (const item of stepAnswerVariables(step)) {
9859
+ const value = answerValue(answers, item.variable);
9860
+ if (value === void 0) continue;
9861
+ items.push({
9862
+ stepId: item.stepId,
9863
+ variable: item.variable,
9864
+ label: item.label,
9865
+ value
9866
+ });
9867
+ }
9868
+ }
9869
+ return { items };
9870
+ }
9871
+ function buildRespondentSnapshot(config, answers) {
9872
+ var _a, _b;
9873
+ const snapshot = {};
9874
+ for (const step of config.steps) {
9875
+ if (step.type !== "input") continue;
9876
+ for (const field of (_a = step.fields) != null ? _a : []) {
9877
+ const variable = (_b = field.variable) != null ? _b : field.id;
9878
+ const value = answerValue(answers, variable);
9879
+ if (value !== void 0) snapshot[variable] = value;
9880
+ }
9881
+ }
9882
+ return snapshot;
9883
+ }
9884
+ function buildContactRef(config, answers) {
9885
+ var _a, _b;
9886
+ const contact = {};
9887
+ for (const step of config.steps) {
9888
+ if (step.type !== "input") continue;
9889
+ for (const field of (_a = step.fields) != null ? _a : []) {
9890
+ const variable = (_b = field.variable) != null ? _b : field.id;
9891
+ const value = answerValue(answers, variable);
9892
+ if (typeof value !== "string" || !value.trim()) continue;
9893
+ const normalizedVariable = variable.toLowerCase().replace(/[-_\s]/g, "");
9894
+ if (field.type === "email" || normalizedVariable.includes("email")) {
9895
+ contact.email = value;
9896
+ } else if (field.type === "tel" || normalizedVariable.includes("phone")) {
9897
+ contact.phone = value;
9898
+ } else if (normalizedVariable === "externalid") {
9899
+ contact.externalId = value;
9900
+ }
9901
+ }
9902
+ }
9903
+ return Object.keys(contact).length > 0 ? contact : void 0;
9904
+ }
9905
+ function eventStepId(event) {
9906
+ if ("step" in event) return event.step.id;
9907
+ if (event.type === "navigate") return event.to.id;
9908
+ if (event.type === "purchase_intent") return event.stepId;
9909
+ return void 0;
9910
+ }
9911
+ function eventProperties(event) {
9912
+ if (event.type === "step_submit") {
9913
+ return { submitted: event.submitted };
9914
+ }
9915
+ if (event.type === "navigate") {
9916
+ return {
9917
+ fromStepId: event.from.id,
9918
+ toStepId: event.to.id,
9919
+ direction: event.direction
9920
+ };
9921
+ }
9922
+ if (event.type === "purchase_intent") {
9923
+ return __spreadValues({
9924
+ variable: event.variable,
9925
+ plan: event.plan
9926
+ }, event.discount ? { discount: event.discount } : {});
9927
+ }
9928
+ return {};
9929
+ }
9930
+ function toCaptureEvent(config, event, sequence, runtime) {
9931
+ const answers = "answers" in event ? event.answers : {};
9932
+ const payload = {
9933
+ id: createCaptureEventId(runtime, event, sequence),
9934
+ type: event.type,
9935
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
9936
+ stepId: eventStepId(event),
9937
+ properties: eventProperties(event),
9938
+ answers: toJsonRecord(answers),
9939
+ computedVariables: "computedVariables" in event ? toJsonRecord(event.computedVariables) : void 0
9940
+ };
9941
+ if (event.type === "complete") {
9942
+ payload.respondentSnapshot = buildRespondentSnapshot(config, answers);
9943
+ payload.answerSnapshot = buildAnswerSnapshot(config, answers);
9944
+ payload.contact = buildContactRef(config, answers);
9945
+ }
9946
+ return payload;
9947
+ }
9948
+ function createCaptureEventId(runtime, event, sequence) {
9949
+ return `${runtime.clientSessionId}:${event.type}:${sequence.toString(
9950
+ 36
9951
+ )}:${randomId("fhqe")}`;
9952
+ }
9953
+ function useJourneyCapture(params) {
9954
+ const onEventRef = React.useRef(params.onEvent);
9955
+ const configRef = React.useRef(params.config);
9956
+ const captureRef = React.useRef(params.capture);
9957
+ const runtimeRef = React.useRef(null);
9958
+ const queueRef = React.useRef([]);
9959
+ const sequenceRef = React.useRef(0);
9960
+ const flushingRef = React.useRef(false);
9961
+ onEventRef.current = params.onEvent;
9962
+ configRef.current = params.config;
9963
+ captureRef.current = params.capture;
9964
+ const persistQueue = React.useCallback(() => {
9965
+ const runtime = runtimeRef.current;
9966
+ if (!runtime || typeof window === "undefined") return;
9967
+ try {
9968
+ sessionStorage.setItem(
9969
+ runtime.queueKey,
9970
+ JSON.stringify(queueRef.current)
9971
+ );
9972
+ } catch (e) {
9973
+ }
9974
+ }, []);
9975
+ const ensureRuntime = React.useCallback((capture) => {
9976
+ if (typeof window === "undefined") return null;
9977
+ const existing = runtimeRef.current;
9978
+ if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId) return existing;
9979
+ const visitorId = getStoredId(
9980
+ localStorage,
9981
+ "fhq_journey_visitor_id",
9982
+ "fhqv"
9983
+ );
9984
+ const clientSessionId = getStoredId(
9985
+ sessionStorage,
9986
+ `fhq_journey_session:${capture.journeyId}`,
9987
+ "fhqs"
9988
+ );
9989
+ const queueKey = `fhq_journey_queue:${capture.journeyId}:${clientSessionId}`;
9990
+ const runtime = {
9991
+ journeyId: capture.journeyId,
9992
+ visitorId,
9993
+ clientSessionId,
9994
+ queueKey
9995
+ };
9996
+ runtimeRef.current = runtime;
9997
+ queueRef.current = readJson(
9998
+ sessionStorage,
9999
+ queueKey,
10000
+ []
10001
+ );
10002
+ return runtime;
10003
+ }, []);
10004
+ const flush = React.useCallback(async () => {
10005
+ var _a, _b, _c;
10006
+ const capture = captureRef.current;
10007
+ if (!capture || !configRef.current || flushingRef.current) return;
10008
+ const runtime = ensureRuntime(capture);
10009
+ if (!runtime || queueRef.current.length === 0) return;
10010
+ flushingRef.current = true;
10011
+ const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
10012
+ const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
10013
+ const batch = queueRef.current.slice(0, batchSize);
10014
+ const baseUrl = (_c = capture.baseUrl) != null ? _c : "https://getfounderhq.com";
10015
+ try {
10016
+ const response = await fetch(
10017
+ `${baseUrl}/api/v1/journeys/${encodeURIComponent(
10018
+ capture.journeyId
10019
+ )}/capture`,
10020
+ {
10021
+ method: "POST",
10022
+ headers: {
10023
+ Authorization: `Bearer ${capture.apiKey}`,
10024
+ "Content-Type": "application/json"
10025
+ },
10026
+ body: JSON.stringify({
10027
+ clientSessionId: runtime.clientSessionId,
10028
+ visitorId: runtime.visitorId,
10029
+ context: captureContext(capture, runtime),
10030
+ events: batch.map((item) => item.event)
10031
+ }),
10032
+ keepalive: batch.length <= 5
10033
+ }
10034
+ );
10035
+ if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
10036
+ queueRef.current = queueRef.current.slice(batch.length);
10037
+ } catch (e) {
10038
+ const failedIds = new Set(batch.map((item) => item.event.id));
10039
+ queueRef.current = queueRef.current.map(
10040
+ (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10041
+ ).filter((item) => item.attempts < maxRetries);
10042
+ } finally {
10043
+ persistQueue();
10044
+ flushingRef.current = false;
10045
+ }
10046
+ }, [ensureRuntime, persistQueue]);
10047
+ React.useEffect(() => {
10048
+ var _a;
10049
+ const capture = captureRef.current;
10050
+ if (!capture || typeof window === "undefined") return;
10051
+ ensureRuntime(capture);
10052
+ const interval = window.setInterval(
10053
+ () => void flush(),
10054
+ Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
10055
+ );
10056
+ const handleVisibility = () => {
10057
+ if (document.visibilityState === "hidden") void flush();
10058
+ };
10059
+ window.addEventListener("beforeunload", persistQueue);
10060
+ document.addEventListener("visibilitychange", handleVisibility);
10061
+ void flush();
10062
+ return () => {
10063
+ window.clearInterval(interval);
10064
+ window.removeEventListener("beforeunload", persistQueue);
10065
+ document.removeEventListener("visibilitychange", handleVisibility);
10066
+ persistQueue();
10067
+ };
10068
+ }, [ensureRuntime, flush, persistQueue, params.capture]);
10069
+ return React.useCallback(
10070
+ (event) => {
10071
+ var _a, _b;
10072
+ (_a = onEventRef.current) == null ? void 0 : _a.call(onEventRef, event);
10073
+ const capture = captureRef.current;
10074
+ const config = configRef.current;
10075
+ if (!capture || !config) return;
10076
+ const runtime = ensureRuntime(capture);
10077
+ if (!runtime) return;
10078
+ sequenceRef.current += 1;
10079
+ queueRef.current.push({
10080
+ event: toCaptureEvent(config, event, sequenceRef.current, runtime),
10081
+ attempts: 0
10082
+ });
10083
+ persistQueue();
10084
+ const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
10085
+ if (event.type === "complete" || queueRef.current.length >= batchSize) {
10086
+ void flush();
10087
+ }
10088
+ },
10089
+ [ensureRuntime, flush, persistQueue]
10090
+ );
10091
+ }
10092
+ var FOUNDERHQ_BASE_URL = "https://getfounderhq.com";
10093
+ function resolveJourneyBaseUrl(baseUrl) {
10094
+ if (!baseUrl) return FOUNDERHQ_BASE_URL;
10095
+ try {
10096
+ const parsed = new URL(baseUrl);
10097
+ const hostname = parsed.hostname.toLowerCase();
10098
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
10099
+ return isLocal ? parsed.origin : FOUNDERHQ_BASE_URL;
10100
+ } catch (e) {
10101
+ return FOUNDERHQ_BASE_URL;
10102
+ }
10103
+ }
9638
10104
  function useJourneyConfig({
9639
10105
  apiKey,
9640
10106
  journeyId,
9641
- baseUrl = "http://localhost:3002"
10107
+ baseUrl,
10108
+ config,
10109
+ capture = true
9642
10110
  }) {
9643
10111
  const [state, setState] = React.useState({ status: "loading" });
9644
10112
  React.useEffect(() => {
9645
10113
  let cancelled = false;
9646
- async function fetchConfig() {
9647
- var _a;
10114
+ const resolvedBaseUrl = resolveJourneyBaseUrl(baseUrl);
10115
+ async function loadConfig() {
10116
+ var _a, _b;
9648
10117
  setState({ status: "loading" });
9649
10118
  try {
9650
- const url = `${baseUrl}/api/v1/journeys/${encodeURIComponent(journeyId)}`;
10119
+ const validateUrl = `${resolvedBaseUrl}/api/v1/journeys/${encodeURIComponent(
10120
+ journeyId
10121
+ )}/validate`;
10122
+ const validateRes = await fetch(validateUrl, {
10123
+ method: "POST",
10124
+ headers: {
10125
+ Authorization: `Bearer ${apiKey}`,
10126
+ "Content-Type": "application/json"
10127
+ },
10128
+ body: JSON.stringify({ capture })
10129
+ });
10130
+ if (!validateRes.ok) {
10131
+ const body = await validateRes.json().catch(() => ({}));
10132
+ throw new Error(
10133
+ (_a = body.error) != null ? _a : `Journey is unavailable (HTTP ${validateRes.status})`
10134
+ );
10135
+ }
10136
+ if (config) {
10137
+ if (!cancelled) {
10138
+ setState({ status: "ready", config });
10139
+ }
10140
+ return;
10141
+ }
10142
+ const url = `${resolvedBaseUrl}/api/v1/journeys/${encodeURIComponent(
10143
+ journeyId
10144
+ )}`;
9651
10145
  const res = await fetch(url, {
9652
10146
  headers: {
9653
10147
  Authorization: `Bearer ${apiKey}`,
@@ -9657,7 +10151,7 @@ function useJourneyConfig({
9657
10151
  if (!res.ok) {
9658
10152
  const body = await res.json().catch(() => ({}));
9659
10153
  throw new Error(
9660
- (_a = body.error) != null ? _a : `Failed to fetch journey config (HTTP ${res.status})`
10154
+ (_b = body.error) != null ? _b : `Failed to fetch journey config (HTTP ${res.status})`
9661
10155
  );
9662
10156
  }
9663
10157
  const data = await res.json();
@@ -9673,11 +10167,11 @@ function useJourneyConfig({
9673
10167
  }
9674
10168
  }
9675
10169
  }
9676
- fetchConfig();
10170
+ loadConfig();
9677
10171
  return () => {
9678
10172
  cancelled = true;
9679
10173
  };
9680
- }, [apiKey, journeyId, baseUrl]);
10174
+ }, [apiKey, journeyId, baseUrl, config, capture]);
9681
10175
  return state;
9682
10176
  }
9683
10177
  function DefaultLoading() {
@@ -9726,15 +10220,16 @@ function DefaultError({ error }) {
9726
10220
  textAlign: "center"
9727
10221
  },
9728
10222
  children: [
9729
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "1.125rem", fontWeight: 600 }, children: "Failed to load journey" }),
10223
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "1.125rem", fontWeight: 600 }, children: "This Journey is unavailable" }),
9730
10224
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.875rem", marginTop: "0.5rem", opacity: 0.7 }, children: error.message })
9731
10225
  ]
9732
10226
  }
9733
10227
  );
9734
10228
  }
9735
- function JourneyRemote({
10229
+ function Journey({
9736
10230
  apiKey,
9737
10231
  journeyId,
10232
+ config,
9738
10233
  baseUrl,
9739
10234
  storageKey,
9740
10235
  onEvent,
@@ -9744,9 +10239,28 @@ function JourneyRemote({
9744
10239
  initialOptions,
9745
10240
  onDiscountCodeApply,
9746
10241
  loadingComponent,
9747
- errorComponent
10242
+ errorComponent,
10243
+ capture
9748
10244
  }) {
9749
- const state = useJourneyConfig({ apiKey, journeyId, baseUrl });
10245
+ const resolvedBaseUrl = resolveJourneyBaseUrl(baseUrl);
10246
+ const captureEnabled = capture !== false;
10247
+ const state = useJourneyConfig({
10248
+ apiKey,
10249
+ journeyId,
10250
+ baseUrl: resolvedBaseUrl,
10251
+ config,
10252
+ capture: captureEnabled
10253
+ });
10254
+ const captureOption = capture === false ? false : __spreadValues({
10255
+ apiKey,
10256
+ journeyId,
10257
+ baseUrl: resolvedBaseUrl
10258
+ }, capture != null ? capture : {});
10259
+ const capturedOnEvent = useJourneyCapture({
10260
+ config: state.status === "ready" ? state.config : null,
10261
+ capture: captureOption,
10262
+ onEvent
10263
+ });
9750
10264
  if (state.status === "loading") {
9751
10265
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent != null ? loadingComponent : /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {}) });
9752
10266
  }
@@ -9759,35 +10273,7 @@ function JourneyRemote({
9759
10273
  config: state.config,
9760
10274
  storageKey,
9761
10275
  theme,
9762
- onEvent,
9763
- initialAnswers,
9764
- initialOptions,
9765
- onDiscountCodeApply,
9766
- children: /* @__PURE__ */ jsxRuntime.jsx(JourneyShell, { className, theme })
9767
- }
9768
- );
9769
- }
9770
- function Journey(props) {
9771
- if ("apiKey" in props && props.apiKey) {
9772
- return /* @__PURE__ */ jsxRuntime.jsx(JourneyRemote, __spreadValues({}, props));
9773
- }
9774
- const {
9775
- config,
9776
- storageKey,
9777
- onEvent,
9778
- className,
9779
- theme,
9780
- initialAnswers,
9781
- initialOptions,
9782
- onDiscountCodeApply
9783
- } = props;
9784
- return /* @__PURE__ */ jsxRuntime.jsx(
9785
- JourneyProvider,
9786
- {
9787
- config,
9788
- storageKey,
9789
- theme,
9790
- onEvent,
10276
+ onEvent: capturedOnEvent,
9791
10277
  initialAnswers,
9792
10278
  initialOptions,
9793
10279
  onDiscountCodeApply,