@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.js CHANGED
@@ -651,6 +651,8 @@ function JourneyProvider({
651
651
  const { trigger } = useWebHaptics();
652
652
  const onEventRef = useRef(onEvent);
653
653
  const rawAnswersRef = useRef({});
654
+ const sessionStartEmittedRef = useRef(false);
655
+ const lastViewedStepIdRef = useRef(null);
654
656
  const [discountCodeDialog, setDiscountCodeDialog] = useState(null);
655
657
  useEffect(() => {
656
658
  onEventRef.current = onEvent;
@@ -710,6 +712,39 @@ function JourneyProvider({
710
712
  () => normalizedConfig.steps.flatMap(getStepVariables),
711
713
  [normalizedConfig.steps]
712
714
  );
715
+ useEffect(() => {
716
+ var _a, _b;
717
+ const currentStep = config.steps[currentStepIndex];
718
+ if (!currentStep) return;
719
+ const eventAnswers = getEventAnswers(
720
+ config.computedVariables,
721
+ rawAnswersRef.current
722
+ );
723
+ const strippedStep = stripOptionsFromEventStep(currentStep);
724
+ if (!sessionStartEmittedRef.current) {
725
+ sessionStartEmittedRef.current = true;
726
+ (_a = onEventRef.current) == null ? void 0 : _a.call(onEventRef, __spreadProps(__spreadValues({
727
+ type: "session_start",
728
+ step: strippedStep
729
+ }, eventAnswers), {
730
+ variables: allVariables
731
+ }));
732
+ }
733
+ if (lastViewedStepIdRef.current !== currentStep.id) {
734
+ lastViewedStepIdRef.current = currentStep.id;
735
+ (_b = onEventRef.current) == null ? void 0 : _b.call(onEventRef, __spreadProps(__spreadValues({
736
+ type: "step_view",
737
+ step: strippedStep
738
+ }, eventAnswers), {
739
+ variables: allVariables
740
+ }));
741
+ }
742
+ }, [
743
+ allVariables,
744
+ config.computedVariables,
745
+ config.steps,
746
+ currentStepIndex
747
+ ]);
713
748
  const setAnswer = useCallback((stepId, answer) => {
714
749
  if (computedVariableIds.has(stepId)) return;
715
750
  setRawAnswers((prev) => {
@@ -2638,9 +2673,11 @@ function ButtonBlock({
2638
2673
  onGoToStep,
2639
2674
  onPurchase,
2640
2675
  onOpenDiscountCode,
2641
- disabled
2676
+ disabled,
2677
+ isLastStep
2642
2678
  }) {
2643
2679
  const handleClick = () => {
2680
+ var _a;
2644
2681
  if (!action) {
2645
2682
  onNext == null ? void 0 : onNext();
2646
2683
  return;
@@ -2660,6 +2697,9 @@ function ButtonBlock({
2660
2697
  }
2661
2698
  break;
2662
2699
  case "link":
2700
+ if ((_a = action.completeBeforeNavigate) != null ? _a : isLastStep) {
2701
+ onNext == null ? void 0 : onNext();
2702
+ }
2663
2703
  if (action.external) {
2664
2704
  window.open(action.url, "_blank", "noopener,noreferrer");
2665
2705
  } else {
@@ -4150,7 +4190,8 @@ function ColumnsBlock({
4150
4190
  answers,
4151
4191
  onNext,
4152
4192
  onBack,
4153
- onGoToStep
4193
+ onGoToStep,
4194
+ isLastStep
4154
4195
  }) {
4155
4196
  return /* @__PURE__ */ jsx("div", { className: "jy-columns-container", children: /* @__PURE__ */ jsx(
4156
4197
  "div",
@@ -4176,6 +4217,7 @@ function ColumnsBlock({
4176
4217
  onBack: onBack != null ? onBack : (() => {
4177
4218
  }),
4178
4219
  onGoToStep,
4220
+ isLastStep,
4179
4221
  wrapInScrollViewport: false
4180
4222
  }
4181
4223
  )
@@ -7851,7 +7893,11 @@ function HiddenBlockSlot({ block }) {
7851
7893
  return /* @__PURE__ */ jsx(
7852
7894
  "div",
7853
7895
  {
7854
- className: cn(widthClass, resolvedMaxWidth ? "mx-auto" : void 0, block.className),
7896
+ className: cn(
7897
+ widthClass,
7898
+ resolvedMaxWidth ? "mx-auto" : void 0,
7899
+ block.className
7900
+ ),
7855
7901
  style: __spreadProps(__spreadValues(__spreadValues({}, resolvedMaxWidth ? { maxWidth: resolvedMaxWidth } : null), block.style), {
7856
7902
  visibility: "hidden"
7857
7903
  }),
@@ -8034,7 +8080,7 @@ function resolveAppliedDiscountForDialog(answers, discountVariable, planId) {
8034
8080
  if (!planId) return answer.planId ? void 0 : answer;
8035
8081
  return discountAppliesToPlan(answer, planId) ? answer : void 0;
8036
8082
  }
8037
- function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToStep, onPurchase, onOpenDiscountCode, inputsValid) {
8083
+ function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToStep, onPurchase, onOpenDiscountCode, inputsValid, isLastStep) {
8038
8084
  var _a, _b;
8039
8085
  const conditionMet = !block.condition || evaluateCondition(block.condition, answers);
8040
8086
  const hasExitAnim = ((_a = block.exitAnimation) == null ? void 0 : _a.preset) && block.exitAnimation.preset !== "none";
@@ -8061,8 +8107,9 @@ function renderBlock(block, i, visibleIndexRef, answers, onNext, onBack, onGoToS
8061
8107
  onGoToStep,
8062
8108
  onPurchase,
8063
8109
  onOpenDiscountCode,
8064
- disabled: buttonDisabledForValidity(resolvedProps, inputsValid)
8065
- }) : block.type === "columns" ? __spreadProps(__spreadValues({}, resolvedProps), { answers, onNext, onBack, onGoToStep }) : block.type === "gravity_bin" ? __spreadProps(__spreadValues({}, resolvedProps), { answers }) : resolvedProps;
8110
+ disabled: buttonDisabledForValidity(resolvedProps, inputsValid),
8111
+ isLastStep
8112
+ }) : block.type === "columns" ? __spreadProps(__spreadValues({}, resolvedProps), { answers, onNext, onBack, onGoToStep, isLastStep }) : block.type === "gravity_bin" ? __spreadProps(__spreadValues({}, resolvedProps), { answers }) : resolvedProps;
8066
8113
  const idx = visibleIndexRef.current;
8067
8114
  visibleIndexRef.current++;
8068
8115
  return /* @__PURE__ */ jsx(
@@ -8084,6 +8131,7 @@ function BlockRenderer({
8084
8131
  onNext,
8085
8132
  onBack,
8086
8133
  onGoToStep,
8134
+ isLastStep,
8087
8135
  scrollViewportRef,
8088
8136
  wrapInScrollViewport = true
8089
8137
  }) {
@@ -8169,7 +8217,8 @@ function BlockRenderer({
8169
8217
  onGoToStep,
8170
8218
  onPurchase,
8171
8219
  onOpenDiscountCode,
8172
- inputsValid
8220
+ inputsValid,
8221
+ isLastStep
8173
8222
  );
8174
8223
  };
8175
8224
  if (!wrapInScrollViewport && stickyBlocks.length === 0) {
@@ -8192,7 +8241,11 @@ function BlockRenderer({
8192
8241
  "div",
8193
8242
  {
8194
8243
  className: layoutClasses(layout),
8195
- style: { gap: `${gap}rem`, maxWidth: layout == null ? void 0 : layout.maxWidth, width: "100%" },
8244
+ style: {
8245
+ gap: `${gap}rem`,
8246
+ maxWidth: layout == null ? void 0 : layout.maxWidth,
8247
+ width: "100%"
8248
+ },
8196
8249
  children: contentBlocks.map((block, i) => renderOne(block, i))
8197
8250
  }
8198
8251
  )
@@ -8203,7 +8256,11 @@ function BlockRenderer({
8203
8256
  ) }) : null
8204
8257
  ] });
8205
8258
  }
8206
- function InfoPageStep({ config, onNext }) {
8259
+ function InfoPageStep({
8260
+ config,
8261
+ onNext,
8262
+ isLastStep
8263
+ }) {
8207
8264
  var _a;
8208
8265
  const { answers } = useJourneyState();
8209
8266
  const { goBack, goToStep } = useJourneyActions();
@@ -8211,9 +8268,7 @@ function InfoPageStep({ config, onNext }) {
8211
8268
  const sortedTimelineEvents = useMemo(
8212
8269
  () => {
8213
8270
  var _a2, _b;
8214
- return [...(_b = (_a2 = config.scrollTimeline) == null ? void 0 : _a2.events) != null ? _b : []].sort(
8215
- (a, b) => a.at - b.at
8216
- );
8271
+ return [...(_b = (_a2 = config.scrollTimeline) == null ? void 0 : _a2.events) != null ? _b : []].sort((a, b) => a.at - b.at);
8217
8272
  },
8218
8273
  [(_a = config.scrollTimeline) == null ? void 0 : _a.events]
8219
8274
  );
@@ -8393,6 +8448,7 @@ function InfoPageStep({ config, onNext }) {
8393
8448
  onNext,
8394
8449
  onBack: goBack,
8395
8450
  onGoToStep: goToStep,
8451
+ isLastStep,
8396
8452
  scrollViewportRef
8397
8453
  }
8398
8454
  );
@@ -9260,7 +9316,7 @@ var STEP_REGISTRY = {
9260
9316
  function StepRenderer({ config }) {
9261
9317
  var _a, _b;
9262
9318
  const { answers } = useJourneyState();
9263
- const { setAnswer, goNext } = useJourneyActions();
9319
+ const { setAnswer, goNext, isLastStep } = useJourneyActions();
9264
9320
  const resolvedConfig = useMemo(() => {
9265
9321
  var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j, _k;
9266
9322
  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(
@@ -9313,7 +9369,8 @@ function StepRenderer({ config }) {
9313
9369
  }
9314
9370
  setAnswer(answerKey, answer);
9315
9371
  },
9316
- onNext: () => goNext()
9372
+ onNext: () => goNext(),
9373
+ isLastStep: isLastStep()
9317
9374
  }
9318
9375
  );
9319
9376
  }
@@ -9610,19 +9667,456 @@ function JourneyShell({ className, theme } = {}) {
9610
9667
  ) })
9611
9668
  ] });
9612
9669
  }
9670
+ var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
9671
+ var JOURNEY_LIBRARY_VERSION = "0.4.1";
9672
+ function randomId(prefix) {
9673
+ const cryptoRef = globalThis.crypto;
9674
+ if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
9675
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
9676
+ }
9677
+ function readJson(storage, key, fallback) {
9678
+ try {
9679
+ const raw = storage.getItem(key);
9680
+ return raw ? JSON.parse(raw) : fallback;
9681
+ } catch (e) {
9682
+ return fallback;
9683
+ }
9684
+ }
9685
+ function getStoredId(storage, key, prefix) {
9686
+ try {
9687
+ const existing = storage.getItem(key);
9688
+ if (existing) return existing;
9689
+ const next = randomId(prefix);
9690
+ storage.setItem(key, next);
9691
+ return next;
9692
+ } catch (e) {
9693
+ return randomId(prefix);
9694
+ }
9695
+ }
9696
+ function isRecord(value) {
9697
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9698
+ }
9699
+ function cleanObject(value) {
9700
+ if (!value || typeof value !== "object") return {};
9701
+ return __spreadValues({}, value);
9702
+ }
9703
+ var DEFAULT_REDACTED_URL_PARAMS = [
9704
+ "access_token",
9705
+ "api_key",
9706
+ "auth",
9707
+ "code",
9708
+ "email",
9709
+ "key",
9710
+ "otp",
9711
+ "password",
9712
+ "phone",
9713
+ "secret",
9714
+ "signature",
9715
+ "token"
9716
+ ];
9717
+ function safeContextFactory(factory) {
9718
+ if (typeof factory !== "function") return void 0;
9719
+ try {
9720
+ const value = factory();
9721
+ return isRecord(value) ? value : void 0;
9722
+ } catch (e) {
9723
+ return void 0;
9724
+ }
9725
+ }
9726
+ function safeTimezone() {
9727
+ try {
9728
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
9729
+ } catch (e) {
9730
+ return void 0;
9731
+ }
9732
+ }
9733
+ function campaignContext(params) {
9734
+ var _a, _b, _c, _d, _e;
9735
+ return cleanObject({
9736
+ source: (_a = params.get("utm_source")) != null ? _a : void 0,
9737
+ medium: (_b = params.get("utm_medium")) != null ? _b : void 0,
9738
+ name: (_c = params.get("utm_campaign")) != null ? _c : void 0,
9739
+ term: (_d = params.get("utm_term")) != null ? _d : void 0,
9740
+ content: (_e = params.get("utm_content")) != null ? _e : void 0
9741
+ });
9742
+ }
9743
+ function redactedSearch(params, redactUrlParams) {
9744
+ if ([...params.keys()].length === 0) return "";
9745
+ const next = new URLSearchParams(params);
9746
+ const redactAll = redactUrlParams === true;
9747
+ const names = Array.isArray(redactUrlParams) && redactUrlParams.length > 0 ? redactUrlParams : DEFAULT_REDACTED_URL_PARAMS;
9748
+ const redactedNames = new Set(names.map((name) => name.toLowerCase()));
9749
+ for (const key of [...next.keys()]) {
9750
+ if (redactAll || redactedNames.has(key.toLowerCase())) {
9751
+ next.set(key, "[redacted]");
9752
+ }
9753
+ }
9754
+ const value = next.toString();
9755
+ return value ? `?${value}` : "";
9756
+ }
9757
+ function pageContext(redactUrlParams) {
9758
+ if (typeof window === "undefined") return void 0;
9759
+ const params = new URLSearchParams(window.location.search);
9760
+ const campaign = campaignContext(params);
9761
+ const search = redactedSearch(params, redactUrlParams);
9762
+ return cleanObject({
9763
+ url: `${window.location.origin}${window.location.pathname}${search}${window.location.hash}`,
9764
+ path: window.location.pathname,
9765
+ search: search || void 0,
9766
+ title: document.title,
9767
+ referrer: document.referrer,
9768
+ campaign
9769
+ });
9770
+ }
9771
+ function captureContext(capture, runtime) {
9772
+ var _a, _b;
9773
+ const customContext = cleanObject(__spreadValues(__spreadValues({}, (_a = capture.context) != null ? _a : {}), (_b = safeContextFactory(capture.contextFactory)) != null ? _b : {}));
9774
+ if (capture.captureContext === false) {
9775
+ return customContext;
9776
+ }
9777
+ const page = pageContext(capture.redactUrlParams);
9778
+ const navigatorRef = typeof window !== "undefined" ? window.navigator : void 0;
9779
+ const screenRef = typeof window !== "undefined" ? window.screen : void 0;
9780
+ return cleanObject(__spreadValues({
9781
+ source: { type: "embed", product: "journeys" },
9782
+ library: {
9783
+ name: JOURNEY_LIBRARY_NAME,
9784
+ version: JOURNEY_LIBRARY_VERSION
9785
+ },
9786
+ anonymousId: runtime.visitorId,
9787
+ sessionId: runtime.clientSessionId,
9788
+ locale: navigatorRef == null ? void 0 : navigatorRef.language,
9789
+ timezone: safeTimezone(),
9790
+ userAgent: navigatorRef == null ? void 0 : navigatorRef.userAgent,
9791
+ page,
9792
+ campaign: isRecord(page) ? page.campaign : void 0,
9793
+ screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
9794
+ }, customContext));
9795
+ }
9796
+ function toJsonRecord(value) {
9797
+ return isRecord(value) ? value : {};
9798
+ }
9799
+ function answerValue(answers, variable) {
9800
+ return Object.prototype.hasOwnProperty.call(answers, variable) ? answers[variable] : void 0;
9801
+ }
9802
+ function stepAnswerVariables(step) {
9803
+ var _a, _b, _c, _d, _e;
9804
+ if (step.type === "swipe_cards") {
9805
+ return ((_a = step.swipeCards) != null ? _a : []).map((card) => ({
9806
+ stepId: step.id,
9807
+ variable: card.variable,
9808
+ label: card.text
9809
+ }));
9810
+ }
9811
+ if (step.type === "input") {
9812
+ return ((_b = step.fields) != null ? _b : []).map((field) => {
9813
+ var _a2;
9814
+ return {
9815
+ stepId: step.id,
9816
+ variable: (_a2 = field.variable) != null ? _a2 : field.id,
9817
+ label: field.label,
9818
+ fieldType: field.type
9819
+ };
9820
+ });
9821
+ }
9822
+ return [
9823
+ {
9824
+ stepId: step.id,
9825
+ variable: (_c = step.variable) != null ? _c : step.id,
9826
+ label: (_e = (_d = step.question) != null ? _d : step.preface) != null ? _e : step.id
9827
+ }
9828
+ ];
9829
+ }
9830
+ function buildAnswerSnapshot(config, answers) {
9831
+ const items = [];
9832
+ for (const step of config.steps) {
9833
+ for (const item of stepAnswerVariables(step)) {
9834
+ const value = answerValue(answers, item.variable);
9835
+ if (value === void 0) continue;
9836
+ items.push({
9837
+ stepId: item.stepId,
9838
+ variable: item.variable,
9839
+ label: item.label,
9840
+ value
9841
+ });
9842
+ }
9843
+ }
9844
+ return { items };
9845
+ }
9846
+ function buildRespondentSnapshot(config, answers) {
9847
+ var _a, _b;
9848
+ const snapshot = {};
9849
+ for (const step of config.steps) {
9850
+ if (step.type !== "input") continue;
9851
+ for (const field of (_a = step.fields) != null ? _a : []) {
9852
+ const variable = (_b = field.variable) != null ? _b : field.id;
9853
+ const value = answerValue(answers, variable);
9854
+ if (value !== void 0) snapshot[variable] = value;
9855
+ }
9856
+ }
9857
+ return snapshot;
9858
+ }
9859
+ function buildContactRef(config, answers) {
9860
+ var _a, _b;
9861
+ const contact = {};
9862
+ for (const step of config.steps) {
9863
+ if (step.type !== "input") continue;
9864
+ for (const field of (_a = step.fields) != null ? _a : []) {
9865
+ const variable = (_b = field.variable) != null ? _b : field.id;
9866
+ const value = answerValue(answers, variable);
9867
+ if (typeof value !== "string" || !value.trim()) continue;
9868
+ const normalizedVariable = variable.toLowerCase().replace(/[-_\s]/g, "");
9869
+ if (field.type === "email" || normalizedVariable.includes("email")) {
9870
+ contact.email = value;
9871
+ } else if (field.type === "tel" || normalizedVariable.includes("phone")) {
9872
+ contact.phone = value;
9873
+ } else if (normalizedVariable === "externalid") {
9874
+ contact.externalId = value;
9875
+ }
9876
+ }
9877
+ }
9878
+ return Object.keys(contact).length > 0 ? contact : void 0;
9879
+ }
9880
+ function eventStepId(event) {
9881
+ if ("step" in event) return event.step.id;
9882
+ if (event.type === "navigate") return event.to.id;
9883
+ if (event.type === "purchase_intent") return event.stepId;
9884
+ return void 0;
9885
+ }
9886
+ function eventProperties(event) {
9887
+ if (event.type === "step_submit") {
9888
+ return { submitted: event.submitted };
9889
+ }
9890
+ if (event.type === "navigate") {
9891
+ return {
9892
+ fromStepId: event.from.id,
9893
+ toStepId: event.to.id,
9894
+ direction: event.direction
9895
+ };
9896
+ }
9897
+ if (event.type === "purchase_intent") {
9898
+ return __spreadValues({
9899
+ variable: event.variable,
9900
+ plan: event.plan
9901
+ }, event.discount ? { discount: event.discount } : {});
9902
+ }
9903
+ return {};
9904
+ }
9905
+ function toCaptureEvent(config, event, sequence, runtime) {
9906
+ const answers = "answers" in event ? event.answers : {};
9907
+ const payload = {
9908
+ id: createCaptureEventId(runtime, event, sequence),
9909
+ type: event.type,
9910
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
9911
+ stepId: eventStepId(event),
9912
+ properties: eventProperties(event),
9913
+ answers: toJsonRecord(answers),
9914
+ computedVariables: "computedVariables" in event ? toJsonRecord(event.computedVariables) : void 0
9915
+ };
9916
+ if (event.type === "complete") {
9917
+ payload.respondentSnapshot = buildRespondentSnapshot(config, answers);
9918
+ payload.answerSnapshot = buildAnswerSnapshot(config, answers);
9919
+ payload.contact = buildContactRef(config, answers);
9920
+ }
9921
+ return payload;
9922
+ }
9923
+ function createCaptureEventId(runtime, event, sequence) {
9924
+ return `${runtime.clientSessionId}:${event.type}:${sequence.toString(
9925
+ 36
9926
+ )}:${randomId("fhqe")}`;
9927
+ }
9928
+ function useJourneyCapture(params) {
9929
+ const onEventRef = useRef(params.onEvent);
9930
+ const configRef = useRef(params.config);
9931
+ const captureRef = useRef(params.capture);
9932
+ const runtimeRef = useRef(null);
9933
+ const queueRef = useRef([]);
9934
+ const sequenceRef = useRef(0);
9935
+ const flushingRef = useRef(false);
9936
+ onEventRef.current = params.onEvent;
9937
+ configRef.current = params.config;
9938
+ captureRef.current = params.capture;
9939
+ const persistQueue = useCallback(() => {
9940
+ const runtime = runtimeRef.current;
9941
+ if (!runtime || typeof window === "undefined") return;
9942
+ try {
9943
+ sessionStorage.setItem(
9944
+ runtime.queueKey,
9945
+ JSON.stringify(queueRef.current)
9946
+ );
9947
+ } catch (e) {
9948
+ }
9949
+ }, []);
9950
+ const ensureRuntime = useCallback((capture) => {
9951
+ if (typeof window === "undefined") return null;
9952
+ const existing = runtimeRef.current;
9953
+ if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId) return existing;
9954
+ const visitorId = getStoredId(
9955
+ localStorage,
9956
+ "fhq_journey_visitor_id",
9957
+ "fhqv"
9958
+ );
9959
+ const clientSessionId = getStoredId(
9960
+ sessionStorage,
9961
+ `fhq_journey_session:${capture.journeyId}`,
9962
+ "fhqs"
9963
+ );
9964
+ const queueKey = `fhq_journey_queue:${capture.journeyId}:${clientSessionId}`;
9965
+ const runtime = {
9966
+ journeyId: capture.journeyId,
9967
+ visitorId,
9968
+ clientSessionId,
9969
+ queueKey
9970
+ };
9971
+ runtimeRef.current = runtime;
9972
+ queueRef.current = readJson(
9973
+ sessionStorage,
9974
+ queueKey,
9975
+ []
9976
+ );
9977
+ return runtime;
9978
+ }, []);
9979
+ const flush = useCallback(async () => {
9980
+ var _a, _b, _c;
9981
+ const capture = captureRef.current;
9982
+ if (!capture || !configRef.current || flushingRef.current) return;
9983
+ const runtime = ensureRuntime(capture);
9984
+ if (!runtime || queueRef.current.length === 0) return;
9985
+ flushingRef.current = true;
9986
+ const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
9987
+ const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
9988
+ const batch = queueRef.current.slice(0, batchSize);
9989
+ const baseUrl = (_c = capture.baseUrl) != null ? _c : "https://getfounderhq.com";
9990
+ try {
9991
+ const response = await fetch(
9992
+ `${baseUrl}/api/v1/journeys/${encodeURIComponent(
9993
+ capture.journeyId
9994
+ )}/capture`,
9995
+ {
9996
+ method: "POST",
9997
+ headers: {
9998
+ Authorization: `Bearer ${capture.apiKey}`,
9999
+ "Content-Type": "application/json"
10000
+ },
10001
+ body: JSON.stringify({
10002
+ clientSessionId: runtime.clientSessionId,
10003
+ visitorId: runtime.visitorId,
10004
+ context: captureContext(capture, runtime),
10005
+ events: batch.map((item) => item.event)
10006
+ }),
10007
+ keepalive: batch.length <= 5
10008
+ }
10009
+ );
10010
+ if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
10011
+ queueRef.current = queueRef.current.slice(batch.length);
10012
+ } catch (e) {
10013
+ const failedIds = new Set(batch.map((item) => item.event.id));
10014
+ queueRef.current = queueRef.current.map(
10015
+ (item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
10016
+ ).filter((item) => item.attempts < maxRetries);
10017
+ } finally {
10018
+ persistQueue();
10019
+ flushingRef.current = false;
10020
+ }
10021
+ }, [ensureRuntime, persistQueue]);
10022
+ useEffect(() => {
10023
+ var _a;
10024
+ const capture = captureRef.current;
10025
+ if (!capture || typeof window === "undefined") return;
10026
+ ensureRuntime(capture);
10027
+ const interval = window.setInterval(
10028
+ () => void flush(),
10029
+ Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
10030
+ );
10031
+ const handleVisibility = () => {
10032
+ if (document.visibilityState === "hidden") void flush();
10033
+ };
10034
+ window.addEventListener("beforeunload", persistQueue);
10035
+ document.addEventListener("visibilitychange", handleVisibility);
10036
+ void flush();
10037
+ return () => {
10038
+ window.clearInterval(interval);
10039
+ window.removeEventListener("beforeunload", persistQueue);
10040
+ document.removeEventListener("visibilitychange", handleVisibility);
10041
+ persistQueue();
10042
+ };
10043
+ }, [ensureRuntime, flush, persistQueue, params.capture]);
10044
+ return useCallback(
10045
+ (event) => {
10046
+ var _a, _b;
10047
+ (_a = onEventRef.current) == null ? void 0 : _a.call(onEventRef, event);
10048
+ const capture = captureRef.current;
10049
+ const config = configRef.current;
10050
+ if (!capture || !config) return;
10051
+ const runtime = ensureRuntime(capture);
10052
+ if (!runtime) return;
10053
+ sequenceRef.current += 1;
10054
+ queueRef.current.push({
10055
+ event: toCaptureEvent(config, event, sequenceRef.current, runtime),
10056
+ attempts: 0
10057
+ });
10058
+ persistQueue();
10059
+ const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
10060
+ if (event.type === "complete" || queueRef.current.length >= batchSize) {
10061
+ void flush();
10062
+ }
10063
+ },
10064
+ [ensureRuntime, flush, persistQueue]
10065
+ );
10066
+ }
10067
+ var FOUNDERHQ_BASE_URL = "https://getfounderhq.com";
10068
+ function resolveJourneyBaseUrl(baseUrl) {
10069
+ if (!baseUrl) return FOUNDERHQ_BASE_URL;
10070
+ try {
10071
+ const parsed = new URL(baseUrl);
10072
+ const hostname = parsed.hostname.toLowerCase();
10073
+ const isLocal = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
10074
+ return isLocal ? parsed.origin : FOUNDERHQ_BASE_URL;
10075
+ } catch (e) {
10076
+ return FOUNDERHQ_BASE_URL;
10077
+ }
10078
+ }
9613
10079
  function useJourneyConfig({
9614
10080
  apiKey,
9615
10081
  journeyId,
9616
- baseUrl = "http://localhost:3002"
10082
+ baseUrl,
10083
+ config,
10084
+ capture = true
9617
10085
  }) {
9618
10086
  const [state, setState] = useState({ status: "loading" });
9619
10087
  useEffect(() => {
9620
10088
  let cancelled = false;
9621
- async function fetchConfig() {
9622
- var _a;
10089
+ const resolvedBaseUrl = resolveJourneyBaseUrl(baseUrl);
10090
+ async function loadConfig() {
10091
+ var _a, _b;
9623
10092
  setState({ status: "loading" });
9624
10093
  try {
9625
- const url = `${baseUrl}/api/v1/journeys/${encodeURIComponent(journeyId)}`;
10094
+ const validateUrl = `${resolvedBaseUrl}/api/v1/journeys/${encodeURIComponent(
10095
+ journeyId
10096
+ )}/validate`;
10097
+ const validateRes = await fetch(validateUrl, {
10098
+ method: "POST",
10099
+ headers: {
10100
+ Authorization: `Bearer ${apiKey}`,
10101
+ "Content-Type": "application/json"
10102
+ },
10103
+ body: JSON.stringify({ capture })
10104
+ });
10105
+ if (!validateRes.ok) {
10106
+ const body = await validateRes.json().catch(() => ({}));
10107
+ throw new Error(
10108
+ (_a = body.error) != null ? _a : `Journey is unavailable (HTTP ${validateRes.status})`
10109
+ );
10110
+ }
10111
+ if (config) {
10112
+ if (!cancelled) {
10113
+ setState({ status: "ready", config });
10114
+ }
10115
+ return;
10116
+ }
10117
+ const url = `${resolvedBaseUrl}/api/v1/journeys/${encodeURIComponent(
10118
+ journeyId
10119
+ )}`;
9626
10120
  const res = await fetch(url, {
9627
10121
  headers: {
9628
10122
  Authorization: `Bearer ${apiKey}`,
@@ -9632,7 +10126,7 @@ function useJourneyConfig({
9632
10126
  if (!res.ok) {
9633
10127
  const body = await res.json().catch(() => ({}));
9634
10128
  throw new Error(
9635
- (_a = body.error) != null ? _a : `Failed to fetch journey config (HTTP ${res.status})`
10129
+ (_b = body.error) != null ? _b : `Failed to fetch journey config (HTTP ${res.status})`
9636
10130
  );
9637
10131
  }
9638
10132
  const data = await res.json();
@@ -9648,11 +10142,11 @@ function useJourneyConfig({
9648
10142
  }
9649
10143
  }
9650
10144
  }
9651
- fetchConfig();
10145
+ loadConfig();
9652
10146
  return () => {
9653
10147
  cancelled = true;
9654
10148
  };
9655
- }, [apiKey, journeyId, baseUrl]);
10149
+ }, [apiKey, journeyId, baseUrl, config, capture]);
9656
10150
  return state;
9657
10151
  }
9658
10152
  function DefaultLoading() {
@@ -9701,15 +10195,16 @@ function DefaultError({ error }) {
9701
10195
  textAlign: "center"
9702
10196
  },
9703
10197
  children: [
9704
- /* @__PURE__ */ jsx("p", { style: { fontSize: "1.125rem", fontWeight: 600 }, children: "Failed to load journey" }),
10198
+ /* @__PURE__ */ jsx("p", { style: { fontSize: "1.125rem", fontWeight: 600 }, children: "This Journey is unavailable" }),
9705
10199
  /* @__PURE__ */ jsx("p", { style: { fontSize: "0.875rem", marginTop: "0.5rem", opacity: 0.7 }, children: error.message })
9706
10200
  ]
9707
10201
  }
9708
10202
  );
9709
10203
  }
9710
- function JourneyRemote({
10204
+ function Journey({
9711
10205
  apiKey,
9712
10206
  journeyId,
10207
+ config,
9713
10208
  baseUrl,
9714
10209
  storageKey,
9715
10210
  onEvent,
@@ -9719,9 +10214,28 @@ function JourneyRemote({
9719
10214
  initialOptions,
9720
10215
  onDiscountCodeApply,
9721
10216
  loadingComponent,
9722
- errorComponent
10217
+ errorComponent,
10218
+ capture
9723
10219
  }) {
9724
- const state = useJourneyConfig({ apiKey, journeyId, baseUrl });
10220
+ const resolvedBaseUrl = resolveJourneyBaseUrl(baseUrl);
10221
+ const captureEnabled = capture !== false;
10222
+ const state = useJourneyConfig({
10223
+ apiKey,
10224
+ journeyId,
10225
+ baseUrl: resolvedBaseUrl,
10226
+ config,
10227
+ capture: captureEnabled
10228
+ });
10229
+ const captureOption = capture === false ? false : __spreadValues({
10230
+ apiKey,
10231
+ journeyId,
10232
+ baseUrl: resolvedBaseUrl
10233
+ }, capture != null ? capture : {});
10234
+ const capturedOnEvent = useJourneyCapture({
10235
+ config: state.status === "ready" ? state.config : null,
10236
+ capture: captureOption,
10237
+ onEvent
10238
+ });
9725
10239
  if (state.status === "loading") {
9726
10240
  return /* @__PURE__ */ jsx(Fragment, { children: loadingComponent != null ? loadingComponent : /* @__PURE__ */ jsx(DefaultLoading, {}) });
9727
10241
  }
@@ -9734,35 +10248,7 @@ function JourneyRemote({
9734
10248
  config: state.config,
9735
10249
  storageKey,
9736
10250
  theme,
9737
- onEvent,
9738
- initialAnswers,
9739
- initialOptions,
9740
- onDiscountCodeApply,
9741
- children: /* @__PURE__ */ jsx(JourneyShell, { className, theme })
9742
- }
9743
- );
9744
- }
9745
- function Journey(props) {
9746
- if ("apiKey" in props && props.apiKey) {
9747
- return /* @__PURE__ */ jsx(JourneyRemote, __spreadValues({}, props));
9748
- }
9749
- const {
9750
- config,
9751
- storageKey,
9752
- onEvent,
9753
- className,
9754
- theme,
9755
- initialAnswers,
9756
- initialOptions,
9757
- onDiscountCodeApply
9758
- } = props;
9759
- return /* @__PURE__ */ jsx(
9760
- JourneyProvider,
9761
- {
9762
- config,
9763
- storageKey,
9764
- theme,
9765
- onEvent,
10251
+ onEvent: capturedOnEvent,
9766
10252
  initialAnswers,
9767
10253
  initialOptions,
9768
10254
  onDiscountCodeApply,