@drawbridge/components 0.1.6 → 0.1.8

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.
@@ -4,13 +4,14 @@ import Link from "./link.mjs";
4
4
  import { Fragment, useCallback, useState } from "react";
5
5
  import { request } from "@drawbridge/drawbridge-utils/fetch";
6
6
  import { useForm, useWatch, Controller } from "../shared/lib/form.mjs";
7
- import { useQuery, useQueryClient } from "@tanstack/react-query";
7
+ import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
8
8
  import yup, { resolver } from "../shared/lib/yup.mjs";
9
9
  import Form from "../shared/form.mjs";
10
10
  import Fieldset from "../shared/fieldset.mjs";
11
11
  import InputGroup from "../input-group.mjs";
12
12
  import Error from "../shared/error.mjs";
13
13
  import InputText from "../input-text.mjs";
14
+ import InputTextarea from "../input-textarea.mjs";
14
15
  import InputUrl from "../shared/input-url.mjs";
15
16
  import InputResource from "./input-resource.mjs";
16
17
  import InputCombobox from "./input-combobox.mjs";
@@ -19,6 +20,7 @@ import { InputArrayButton, InputArrayControl, InputArraySortable } from "./input
19
20
  import Badge from "../shared/badge.mjs";
20
21
  import CardTask from "./card-task.mjs";
21
22
  import { LoaderDynamic } from "../shared/loader.mjs";
23
+ import { smsNumberActive } from "./lib/sms.mjs";
22
24
  import Trash from "../icons/trash.mjs";
23
25
  import Button from "../shared/button.mjs";
24
26
  const emailSteps = ["step.email.send", "step.email.digest"];
@@ -195,32 +197,45 @@ const WorkflowTitleField = ({ control, formState }) => /* @__PURE__ */ jsx(
195
197
  }
196
198
  );
197
199
  const campaignFilterId = (campaign) => (campaign == null ? void 0 : campaign.id) || campaign || null;
200
+ const campaignFilterIds = (filters) => (Array.isArray(filters == null ? void 0 : filters.campaigns) ? filters.campaigns : [filters == null ? void 0 : filters.campaign]).map(campaignFilterId).filter(Boolean);
201
+ const filterRows = (ids, docs) => ids.map((id) => docs.find((doc) => (doc == null ? void 0 : doc.id) === id) || { id });
202
+ const filterSeed = (seeds, id) => (seeds || []).find((doc) => (doc == null ? void 0 : doc.id) === id);
203
+ const filterMissing = (error) => {
204
+ if ((error == null ? void 0 : error.status) === 404) return { result: null };
205
+ throw error;
206
+ };
198
207
  const WorkflowFilterField = ({
199
208
  collection,
200
209
  control,
201
210
  label,
202
211
  message,
203
212
  model,
213
+ multiple = false,
204
214
  name,
205
215
  organization,
206
- resource
216
+ resource,
217
+ seeds
207
218
  }) => {
208
219
  const queryClient = useQueryClient();
209
- const id = useWatch({
220
+ const value = useWatch({
210
221
  control,
211
222
  name
212
223
  });
213
- const selected = useQuery({
214
- queryKey: ["workflow-filter", model, organization, id],
215
- queryFn: async () => {
216
- const { result } = await request({
217
- endpoint: "/organization/" + organization + "/" + resource + "/" + id
218
- }).catch(() => ({ result: null }));
219
- return result;
220
- },
221
- enabled: Boolean(id),
222
- staleTime: Infinity
223
- });
224
+ const ids = multiple ? value || [] : [value].filter(Boolean);
225
+ const docs = useQueries({
226
+ queries: ids.map((id) => ({
227
+ queryKey: ["workflow-filter", model, organization, id],
228
+ queryFn: async () => {
229
+ const { result } = await request({
230
+ endpoint: "/organization/" + organization + "/" + resource + "/" + id
231
+ }).catch(filterMissing);
232
+ return result;
233
+ },
234
+ initialData: () => filterSeed(seeds, id),
235
+ staleTime: Infinity
236
+ }))
237
+ }).map((query) => query.data);
238
+ const seed = (doc) => (doc == null ? void 0 : doc.id) && queryClient.setQueryData(["workflow-filter", model, organization, doc.id], doc);
224
239
  return /* @__PURE__ */ jsx(
225
240
  Controller,
226
241
  {
@@ -237,11 +252,15 @@ const WorkflowFilterField = ({
237
252
  ...{
238
253
  endpoint: "/organization/" + organization + "/" + collection,
239
254
  model,
240
- onChange: (doc) => {
241
- if (doc == null ? void 0 : doc.id) queryClient.setQueryData(["workflow-filter", model, organization, doc.id], doc);
255
+ multiple,
256
+ onChange: multiple ? (picked) => {
257
+ picked.forEach(seed);
258
+ onChange(picked.map((doc) => doc == null ? void 0 : doc.id).filter(Boolean));
259
+ } : (doc) => {
260
+ seed(doc);
242
261
  onChange((doc == null ? void 0 : doc.id) || null);
243
262
  },
244
- value: selected.data || null
263
+ value: multiple ? filterRows(ids, docs) : docs[0] || null
245
264
  }
246
265
  }
247
266
  ),
@@ -328,7 +347,7 @@ const FormOrganizationWorkflow = ({
328
347
  // Both blank, meaning every campaign and every segment. The
329
348
  // create drawer asks for a trigger and a title only; narrowing
330
349
  // is done on the workflow page afterwards.
331
- campaign: null,
350
+ campaigns: [],
332
351
  segment: null
333
352
  }
334
353
  }
@@ -397,6 +416,48 @@ const FormOrganizationWorkflow = ({
397
416
  }
398
417
  );
399
418
  };
419
+ const workflowSchema = yup.object({
420
+ title: yup.string().required().max(100).label("Title"),
421
+ triggerEvent: yup.string().label("Trigger").required("A trigger is required"),
422
+ steps: yup.array().of(
423
+ yup.object({
424
+ type: yup.string().label("Step").required("A step is required"),
425
+ url: yup.string().when("type", ([type], schema) => {
426
+ if (type === "step.webhook.send") {
427
+ return schema.url().required("URL is required").label("URL");
428
+ }
429
+ return schema;
430
+ }),
431
+ subject: yup.string().when("type", ([type], schema) => {
432
+ if (emailSteps.includes(type)) {
433
+ return schema.required("Subject is required").label("Subject");
434
+ }
435
+ return schema;
436
+ }),
437
+ message: yup.string().when("type", ([type], schema) => {
438
+ if (type === "step.sms.send") {
439
+ return schema.required("Message is required").max(1500).label("Message");
440
+ }
441
+ if (messageSteps.includes(type)) {
442
+ return schema.required("Message is required").label("Message");
443
+ }
444
+ return schema;
445
+ }),
446
+ // No minimum. The organization owner is always a recipient, so a
447
+ // step with no members chosen is complete — requiring one here
448
+ // made the step unsaveable for a solo merchant, who has no
449
+ // member documents to choose and cannot reach the members
450
+ // endpoint at all.
451
+ members: yup.array().label("Recipients"),
452
+ discount: yup.object().nullable().when("type", ([type], schema) => {
453
+ if (type === "step.commerce.code.issue") {
454
+ return schema.required("Discount is required").label("Discount");
455
+ }
456
+ return schema;
457
+ })
458
+ })
459
+ )
460
+ });
400
461
  const FormOrganizationWorkflowEdit = ({
401
462
  disabled,
402
463
  onClose,
@@ -404,11 +465,13 @@ const FormOrganizationWorkflowEdit = ({
404
465
  onSuccess,
405
466
  values
406
467
  }) => {
407
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
468
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
408
469
  const { catalog, catalogList, triggerOptions } = useWorkflowCatalog(values == null ? void 0 : values.organization, (_a = values == null ? void 0 : values.trigger) == null ? void 0 : _a.event);
409
470
  const { items: memberItems, loading: membersLoading } = useOrganizationMembers(values == null ? void 0 : values.organization);
410
471
  const queryClient = useQueryClient();
411
- const ownerEmail = queryClient.getQueryData(["organization", values == null ? void 0 : values.organization]).owner.email;
472
+ const organizationData = queryClient.getQueryData(["organization", values == null ? void 0 : values.organization]);
473
+ const ownerEmail = organizationData.owner.email;
474
+ const smsActive = smsNumberActive(organizationData);
412
475
  const commerceConnectionId = ((_b = catalogList.flatMap((item) => item.steps || []).find(
413
476
  (s) => s.value === "step.commerce.customer.insert" || s.value === "step.commerce.code.issue"
414
477
  )) == null ? void 0 : _b.connectionId) || null;
@@ -436,55 +499,15 @@ const FormOrganizationWorkflowEdit = ({
436
499
  const { control, formState, getValues, handleSubmit, setValue } = useForm({
437
500
  values: {
438
501
  steps: defaultSteps,
439
- filterCampaign: campaignFilterId((_h = (_g = values == null ? void 0 : values.trigger) == null ? void 0 : _g.filters) == null ? void 0 : _h.campaign),
502
+ filterCampaigns: campaignFilterIds((_g = values == null ? void 0 : values.trigger) == null ? void 0 : _g.filters),
440
503
  // Stored and returned as a plain id — extend.workflow() $lookup-hydrates
441
504
  // only the campaign — but read through the same reducer, so the field
442
505
  // stays right if the segment is ever hydrated too.
443
- filterSegment: campaignFilterId((_j = (_i = values == null ? void 0 : values.trigger) == null ? void 0 : _i.filters) == null ? void 0 : _j.segment),
506
+ filterSegment: campaignFilterId((_i = (_h = values == null ? void 0 : values.trigger) == null ? void 0 : _h.filters) == null ? void 0 : _i.segment),
444
507
  title: (values == null ? void 0 : values.title) || "",
445
- triggerEvent: ((_k = values == null ? void 0 : values.trigger) == null ? void 0 : _k.event) || ""
508
+ triggerEvent: ((_j = values == null ? void 0 : values.trigger) == null ? void 0 : _j.event) || ""
446
509
  },
447
- resolver: resolver(
448
- yup.object({
449
- title: yup.string().required().max(100).label("Title"),
450
- triggerEvent: yup.string().label("Trigger").required("A trigger is required"),
451
- steps: yup.array().of(
452
- yup.object({
453
- type: yup.string().label("Step").required("A step is required"),
454
- url: yup.string().when("type", ([type], schema) => {
455
- if (type === "step.webhook.send") {
456
- return schema.url().required("URL is required").label("URL");
457
- }
458
- return schema;
459
- }),
460
- subject: yup.string().when("type", ([type], schema) => {
461
- if (messageSteps.includes(type)) {
462
- return schema.required("Subject is required").label("Subject");
463
- }
464
- return schema;
465
- }),
466
- message: yup.string().when("type", ([type], schema) => {
467
- if (messageSteps.includes(type)) {
468
- return schema.required("Message is required").label("Message");
469
- }
470
- return schema;
471
- }),
472
- // No minimum. The organization owner is always a recipient, so a
473
- // step with no members chosen is complete — requiring one here
474
- // made the step unsaveable for a solo merchant, who has no
475
- // member documents to choose and cannot reach the members
476
- // endpoint at all.
477
- members: yup.array().label("Recipients"),
478
- discount: yup.object().nullable().when("type", ([type], schema) => {
479
- if (type === "step.commerce.code.issue") {
480
- return schema.required("Discount is required").label("Discount");
481
- }
482
- return schema;
483
- })
484
- })
485
- )
486
- })
487
- )
510
+ resolver: resolver(workflowSchema)
488
511
  });
489
512
  const watchedTrigger = useWatch({
490
513
  control,
@@ -494,7 +517,7 @@ const FormOrganizationWorkflowEdit = ({
494
517
  control,
495
518
  name: "steps"
496
519
  });
497
- const issueIndex = ((_l = values == null ? void 0 : values.issue) == null ? void 0 : _l.step) ? (watchedSteps || []).findIndex((step) => (step == null ? void 0 : step.type) === values.issue.step) : -1;
520
+ const issueIndex = ((_k = values == null ? void 0 : values.issue) == null ? void 0 : _k.step) ? (watchedSteps || []).findIndex((step) => (step == null ? void 0 : step.type) === values.issue.step) : -1;
498
521
  const selectedTriggerEntry = catalogList.find((item) => item.trigger === watchedTrigger) || null;
499
522
  const getAvailableVariables = (index) => {
500
523
  return watchedSteps.slice(0, index).flatMap((watchedStep) => {
@@ -534,6 +557,22 @@ const FormOrganizationWorkflowEdit = ({
534
557
  href: "/organizations/" + (values == null ? void 0 : values.organization) + "/connections/" + step.slug,
535
558
  onClick: onClose
536
559
  }
560
+ } : {},
561
+ // The same link-shaped option, greyed: it cannot be picked, and
562
+ // clicking it opens Settings, where the number is bought.
563
+ ...step.value === "step.sms.send" && !smsActive ? {
564
+ badge: {
565
+ text: "Settings"
566
+ },
567
+ className: "opacity-50",
568
+ disabled: false,
569
+ element: Link,
570
+ key: step == null ? void 0 : step.key,
571
+ props: {
572
+ "aria-disabled": true,
573
+ href: "/organizations/" + (values == null ? void 0 : values.organization) + "/settings",
574
+ onClick: onClose
575
+ }
537
576
  } : {}
538
577
  }))
539
578
  ];
@@ -580,7 +619,7 @@ const FormOrganizationWorkflowEdit = ({
580
619
  };
581
620
  const onSubmit = async ({
582
621
  steps,
583
- filterCampaign,
622
+ filterCampaigns,
584
623
  filterSegment,
585
624
  title,
586
625
  triggerEvent
@@ -590,10 +629,8 @@ const FormOrganizationWorkflowEdit = ({
590
629
  steps: steps.map(({ connectionId, discount, members, message, score, slug, subject, text, type, url }, index) => {
591
630
  const settings = {};
592
631
  if (type === "step.webhook.send") settings.url = url;
593
- if (messageSteps.includes(type)) {
594
- settings.subject = subject;
595
- settings.message = message;
596
- }
632
+ if (emailSteps.includes(type)) settings.subject = subject;
633
+ if (messageSteps.includes(type)) settings.message = message;
597
634
  if (emailSteps.includes(type)) {
598
635
  if (score && score.subject === subject && score.message === message) settings.score = score;
599
636
  else if (score) setValue("steps." + index + ".score", null);
@@ -616,7 +653,7 @@ const FormOrganizationWorkflowEdit = ({
616
653
  trigger: {
617
654
  event: triggerEvent,
618
655
  filters: {
619
- campaign: filterCampaign || null,
656
+ campaigns: filterCampaigns || [],
620
657
  segment: filterSegment || null
621
658
  }
622
659
  }
@@ -686,12 +723,14 @@ const FormOrganizationWorkflowEdit = ({
686
723
  ...{
687
724
  collection: "campaigns",
688
725
  control,
689
- label: "Campaign",
690
- message: "Only trigger for leads from a specific campaign. Leaving blank will trigger for all campaigns.",
726
+ label: "Campaigns",
727
+ message: "Only runs for leads from the campaigns you add. With none added, it runs for every campaign.",
691
728
  model: "campaign",
692
- name: "filterCampaign",
729
+ multiple: true,
730
+ name: "filterCampaigns",
693
731
  organization: values == null ? void 0 : values.organization,
694
- resource: "campaign"
732
+ resource: "campaign",
733
+ seeds: (_m = (_l = values == null ? void 0 : values.trigger) == null ? void 0 : _l.filters) == null ? void 0 : _m.campaigns
695
734
  }
696
735
  }
697
736
  ),
@@ -714,7 +753,7 @@ const FormOrganizationWorkflowEdit = ({
714
753
  InputGroup,
715
754
  {
716
755
  ...{
717
- error: (_n = (_m = formState == null ? void 0 : formState.errors) == null ? void 0 : _m.steps) == null ? void 0 : _n.message,
756
+ error: (_o = (_n = formState == null ? void 0 : formState.errors) == null ? void 0 : _n.steps) == null ? void 0 : _o.message,
718
757
  input: /* @__PURE__ */ jsx(
719
758
  InputArraySortable,
720
759
  {
@@ -944,36 +983,6 @@ const FormOrganizationWorkflowEdit = ({
944
983
  }
945
984
  }
946
985
  ),
947
- step.type === "step.sms.send" && /* @__PURE__ */ jsx(
948
- Controller,
949
- {
950
- ...{
951
- control,
952
- name: `steps.${index}.subject`,
953
- render: ({ field: { value, onChange } }) => {
954
- var _a2, _b2, _c2, _d2;
955
- return /* @__PURE__ */ jsx(
956
- InputGroup,
957
- {
958
- ...{
959
- error: (_d2 = (_c2 = (_b2 = (_a2 = formState == null ? void 0 : formState.errors) == null ? void 0 : _a2.steps) == null ? void 0 : _b2[index]) == null ? void 0 : _c2.subject) == null ? void 0 : _d2.message,
960
- input: /* @__PURE__ */ jsx(
961
- InputText,
962
- {
963
- ...{
964
- onChange,
965
- value: value || ""
966
- }
967
- }
968
- ),
969
- label: "Subject"
970
- }
971
- }
972
- );
973
- }
974
- }
975
- }
976
- ),
977
986
  step.type === "step.sms.send" && /* @__PURE__ */ jsx(
978
987
  Controller,
979
988
  {
@@ -988,10 +997,11 @@ const FormOrganizationWorkflowEdit = ({
988
997
  ...{
989
998
  error: (_d2 = (_c2 = (_b2 = (_a2 = formState == null ? void 0 : formState.errors) == null ? void 0 : _a2.steps) == null ? void 0 : _b2[index]) == null ? void 0 : _c2.message) == null ? void 0 : _d2.message,
990
999
  input: /* @__PURE__ */ jsx(
991
- InputText,
1000
+ InputTextarea,
992
1001
  {
993
1002
  ...{
994
1003
  onChange,
1004
+ rows: 3,
995
1005
  value: value || ""
996
1006
  }
997
1007
  }
@@ -1105,5 +1115,10 @@ export {
1105
1115
  FormOrganizationWorkflow,
1106
1116
  FormOrganizationWorkflowEdit,
1107
1117
  campaignFilterId,
1108
- stepRow
1118
+ campaignFilterIds,
1119
+ filterMissing,
1120
+ filterRows,
1121
+ filterSeed,
1122
+ stepRow,
1123
+ workflowSchema
1109
1124
  };
@@ -17,7 +17,10 @@ var __copyProps = (to, from, except, desc) => {
17
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
18
  var navigation_exports = {};
19
19
  __export(navigation_exports, {
20
- interceptHref: () => interceptHref
20
+ expiredSessionHref: () => expiredSessionHref,
21
+ interceptHref: () => interceptHref,
22
+ retrySession: () => retrySession,
23
+ sessionRetryKey: () => sessionRetryKey
21
24
  });
22
25
  module.exports = __toCommonJS(navigation_exports);
23
26
  const interceptHref = ({
@@ -40,7 +43,24 @@ const interceptHref = ({
40
43
  if (url.pathname === location.pathname && url.search === location.search) return null;
41
44
  return url.pathname + url.search + url.hash;
42
45
  };
46
+ const expiredSessionHref = ({
47
+ csrf,
48
+ pathname = "/",
49
+ search = ""
50
+ }) => {
51
+ if (pathname === "/auth" || pathname.startsWith("/auth/")) return null;
52
+ const back = "/auth?redirectTo=" + encodeURIComponent(pathname + search);
53
+ return "/api/auth/signout?redirectTo=" + encodeURIComponent(back) + (csrf ? "&csrf=" + encodeURIComponent(csrf) : "");
54
+ };
55
+ const sessionRetryKey = "drawbridge.session.retry";
56
+ const retrySession = ({
57
+ now,
58
+ stamp
59
+ }) => !(Number(stamp) > now - 6e4);
43
60
  // Annotate the CommonJS export names for ESM import in node:
44
61
  0 && (module.exports = {
45
- interceptHref
62
+ expiredSessionHref,
63
+ interceptHref,
64
+ retrySession,
65
+ sessionRetryKey
46
66
  });
@@ -18,6 +18,23 @@ const interceptHref = ({
18
18
  if (url.pathname === location.pathname && url.search === location.search) return null;
19
19
  return url.pathname + url.search + url.hash;
20
20
  };
21
+ const expiredSessionHref = ({
22
+ csrf,
23
+ pathname = "/",
24
+ search = ""
25
+ }) => {
26
+ if (pathname === "/auth" || pathname.startsWith("/auth/")) return null;
27
+ const back = "/auth?redirectTo=" + encodeURIComponent(pathname + search);
28
+ return "/api/auth/signout?redirectTo=" + encodeURIComponent(back) + (csrf ? "&csrf=" + encodeURIComponent(csrf) : "");
29
+ };
30
+ const sessionRetryKey = "drawbridge.session.retry";
31
+ const retrySession = ({
32
+ now,
33
+ stamp
34
+ }) => !(Number(stamp) > now - 6e4);
21
35
  export {
22
- interceptHref
36
+ expiredSessionHref,
37
+ interceptHref,
38
+ retrySession,
39
+ sessionRetryKey
23
40
  };
@@ -0,0 +1,27 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var sms_exports = {};
19
+ __export(sms_exports, {
20
+ smsNumberActive: () => smsNumberActive
21
+ });
22
+ module.exports = __toCommonJS(sms_exports);
23
+ const smsNumberActive = (organization) => ((organization == null ? void 0 : organization.identityItems) || []).some((row) => (row == null ? void 0 : row.type) === "sms" && (row == null ? void 0 : row.status) === "verified");
24
+ // Annotate the CommonJS export names for ESM import in node:
25
+ 0 && (module.exports = {
26
+ smsNumberActive
27
+ });
@@ -0,0 +1,4 @@
1
+ const smsNumberActive = (organization) => ((organization == null ? void 0 : organization.identityItems) || []).some((row) => (row == null ? void 0 : row.type) === "sms" && (row == null ? void 0 : row.status) === "verified");
2
+ export {
3
+ smsNumberActive
4
+ };
@@ -37,6 +37,7 @@ var import_react_query = require("@tanstack/react-query");
37
37
  var import_fetch = require("@drawbridge/drawbridge-utils/fetch");
38
38
  var import_socket = require("socket.io-client");
39
39
  var import_template_not_found = __toESM(require("./template-not-found"));
40
+ var import_navigation = require("./lib/navigation");
40
41
  var import_drawer = __toESM(require("./drawer"));
41
42
  var import_guard_navigation = __toESM(require("./guard-navigation"));
42
43
  var import_modal = __toESM(require("./modal"));
@@ -46,7 +47,7 @@ const ProviderUi = ({
46
47
  children,
47
48
  token
48
49
  }) => {
49
- var _a, _b, _c, _d;
50
+ var _a, _b, _c, _d, _e, _f;
50
51
  const queryClient = (0, import_react_query.useQueryClient)();
51
52
  const tokenRef = (0, import_react.useRef)(token);
52
53
  (0, import_react.useEffect)(
@@ -171,7 +172,31 @@ const ProviderUi = ({
171
172
  }
172
173
  ]
173
174
  });
174
- if ((account == null ? void 0 : account.isLoading) || (app == null ? void 0 : app.isLoading)) {
175
+ const expired = ((_a = account == null ? void 0 : account.error) == null ? void 0 : _a.status) === 401 && typeof window !== "undefined" ? (0, import_navigation.expiredSessionHref)({
176
+ csrf: (_b = document.cookie.match(/(?:^|;\s*)drawbridge\.csrf=([^;]+)/)) == null ? void 0 : _b[1],
177
+ pathname: window.location.pathname,
178
+ search: window.location.search
179
+ }) : null;
180
+ (0, import_react.useEffect)(
181
+ () => {
182
+ if (!expired) return;
183
+ const signOut = () => window.location.replace(expired);
184
+ try {
185
+ if (!(0, import_navigation.retrySession)({ now: Date.now(), stamp: window.sessionStorage.getItem(import_navigation.sessionRetryKey) })) return signOut();
186
+ window.sessionStorage.setItem(import_navigation.sessionRetryKey, String(Date.now()));
187
+ } catch {
188
+ return signOut();
189
+ }
190
+ fetch(
191
+ "/api/auth/refresh",
192
+ {
193
+ method: "POST"
194
+ }
195
+ ).then((response) => response.ok ? window.location.reload() : signOut()).catch(signOut);
196
+ },
197
+ [expired]
198
+ );
199
+ if ((account == null ? void 0 : account.isLoading) || (app == null ? void 0 : app.isLoading) || expired) {
175
200
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_loader.LogoLoader, {});
176
201
  }
177
202
  ;
@@ -182,7 +207,7 @@ const ProviderUi = ({
182
207
  ...{
183
208
  button: false,
184
209
  href: false,
185
- message: (_a = app == null ? void 0 : app.error) == null ? void 0 : _a.message
210
+ message: (_c = app == null ? void 0 : app.error) == null ? void 0 : _c.message
186
211
  }
187
212
  }
188
213
  );
@@ -195,7 +220,7 @@ const ProviderUi = ({
195
220
  {
196
221
  ...{
197
222
  button: false,
198
- message: (_b = account == null ? void 0 : account.error) == null ? void 0 : _b.message
223
+ message: (_d = account == null ? void 0 : account.error) == null ? void 0 : _d.message
199
224
  }
200
225
  }
201
226
  );
@@ -207,7 +232,7 @@ const ProviderUi = ({
207
232
  ...{
208
233
  button: "Sign in",
209
234
  href: "/auth",
210
- message: (_c = account == null ? void 0 : account.error) == null ? void 0 : _c.message
235
+ message: (_e = account == null ? void 0 : account.error) == null ? void 0 : _e.message
211
236
  }
212
237
  }
213
238
  );
@@ -227,7 +252,7 @@ const ProviderUi = ({
227
252
  })
228
253
  )
229
254
  },
230
- children: (_d = drawer == null ? void 0 : drawer.data) == null ? void 0 : _d.children
255
+ children: (_f = drawer == null ? void 0 : drawer.data) == null ? void 0 : _f.children
231
256
  }
232
257
  ),
233
258
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -5,6 +5,7 @@ import { useQueries, useQueryClient } from "@tanstack/react-query";
5
5
  import { isTransientError, request, setMemoryToken } from "@drawbridge/drawbridge-utils/fetch";
6
6
  import { io } from "socket.io-client";
7
7
  import TemplateNotFound from "./template-not-found.mjs";
8
+ import { expiredSessionHref, retrySession, sessionRetryKey } from "./lib/navigation.mjs";
8
9
  import Drawer from "./drawer.mjs";
9
10
  import GuardNavigation from "./guard-navigation.mjs";
10
11
  import Modal from "./modal.mjs";
@@ -14,7 +15,7 @@ const ProviderUi = ({
14
15
  children,
15
16
  token
16
17
  }) => {
17
- var _a, _b, _c, _d;
18
+ var _a, _b, _c, _d, _e, _f;
18
19
  const queryClient = useQueryClient();
19
20
  const tokenRef = useRef(token);
20
21
  useEffect(
@@ -139,7 +140,31 @@ const ProviderUi = ({
139
140
  }
140
141
  ]
141
142
  });
142
- if ((account == null ? void 0 : account.isLoading) || (app == null ? void 0 : app.isLoading)) {
143
+ const expired = ((_a = account == null ? void 0 : account.error) == null ? void 0 : _a.status) === 401 && typeof window !== "undefined" ? expiredSessionHref({
144
+ csrf: (_b = document.cookie.match(/(?:^|;\s*)drawbridge\.csrf=([^;]+)/)) == null ? void 0 : _b[1],
145
+ pathname: window.location.pathname,
146
+ search: window.location.search
147
+ }) : null;
148
+ useEffect(
149
+ () => {
150
+ if (!expired) return;
151
+ const signOut = () => window.location.replace(expired);
152
+ try {
153
+ if (!retrySession({ now: Date.now(), stamp: window.sessionStorage.getItem(sessionRetryKey) })) return signOut();
154
+ window.sessionStorage.setItem(sessionRetryKey, String(Date.now()));
155
+ } catch {
156
+ return signOut();
157
+ }
158
+ fetch(
159
+ "/api/auth/refresh",
160
+ {
161
+ method: "POST"
162
+ }
163
+ ).then((response) => response.ok ? window.location.reload() : signOut()).catch(signOut);
164
+ },
165
+ [expired]
166
+ );
167
+ if ((account == null ? void 0 : account.isLoading) || (app == null ? void 0 : app.isLoading) || expired) {
143
168
  return /* @__PURE__ */ jsx(LogoLoader, {});
144
169
  }
145
170
  ;
@@ -150,7 +175,7 @@ const ProviderUi = ({
150
175
  ...{
151
176
  button: false,
152
177
  href: false,
153
- message: (_a = app == null ? void 0 : app.error) == null ? void 0 : _a.message
178
+ message: (_c = app == null ? void 0 : app.error) == null ? void 0 : _c.message
154
179
  }
155
180
  }
156
181
  );
@@ -163,7 +188,7 @@ const ProviderUi = ({
163
188
  {
164
189
  ...{
165
190
  button: false,
166
- message: (_b = account == null ? void 0 : account.error) == null ? void 0 : _b.message
191
+ message: (_d = account == null ? void 0 : account.error) == null ? void 0 : _d.message
167
192
  }
168
193
  }
169
194
  );
@@ -175,7 +200,7 @@ const ProviderUi = ({
175
200
  ...{
176
201
  button: "Sign in",
177
202
  href: "/auth",
178
- message: (_c = account == null ? void 0 : account.error) == null ? void 0 : _c.message
203
+ message: (_e = account == null ? void 0 : account.error) == null ? void 0 : _e.message
179
204
  }
180
205
  }
181
206
  );
@@ -195,7 +220,7 @@ const ProviderUi = ({
195
220
  })
196
221
  )
197
222
  },
198
- children: (_d = drawer == null ? void 0 : drawer.data) == null ? void 0 : _d.children
223
+ children: (_f = drawer == null ? void 0 : drawer.data) == null ? void 0 : _f.children
199
224
  }
200
225
  ),
201
226
  /* @__PURE__ */ jsx(