@burdenoff/microfe-brainyrich 2026.1025.2 → 2026.1028.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AggregationPanel.d.ts","sourceRoot":"","sources":["../../../src/brainyrich/components/AggregationPanel.tsx"],"names":[],"mappings":"AA+CA,wBAAgB,gBAAgB,4CA+M/B"}
1
+ {"version":3,"file":"AggregationPanel.d.ts","sourceRoot":"","sources":["../../../src/brainyrich/components/AggregationPanel.tsx"],"names":[],"mappings":"AA4DA,wBAAgB,gBAAgB,4CA8R/B"}
@@ -2,172 +2,214 @@ import { errorMessage as e, notify as t } from "./notify.js";
2
2
  import { ConfirmDialog as n } from "./ConfirmDialog.js";
3
3
  import { useTr as r } from "../hooks/useTr.js";
4
4
  import { ErrorState as i, LoadingState as a, formatDate as o } from "../pages/_shared.js";
5
- import { useAggregationLinks as s, useAggregationProviders as c, useCreatePlaidLinkToken as l, useExchangePlaidPublicToken as u, useRevokeAggregationLink as d, useSyncAggregationLink as f } from "../hooks/useAggregation.js";
6
- import { openPlaidLink as p } from "../utils/plaidLink.js";
7
- import { useState as m } from "react";
8
- import { Building2 as h, Link2 as g, RefreshCw as _, ShieldCheck as v, Unlink as y } from "lucide-react";
9
- import { jsx as b, jsxs as x } from "react/jsx-runtime";
10
- import { Badge as S, Button as C, Card as w, CardContent as T, CardDescription as E, CardHeader as D, CardTitle as O } from "@burdenoff/fe-libs/ui";
5
+ import { useAggregationLinks as s, useAggregationProviders as c, useCompletePlaidReauthentication as l, useCreatePlaidLinkToken as u, useCreatePlaidUpdateLinkToken as d, useExchangePlaidPublicToken as f, useRevokeAggregationLink as p, useSyncAggregationLink as m } from "../hooks/useAggregation.js";
6
+ import { openPlaidLink as h } from "../utils/plaidLink.js";
7
+ import { useState as g } from "react";
8
+ import { AlertTriangle as _, Building2 as v, KeyRound as y, Link2 as b, RefreshCw as x, ShieldCheck as S, Unlink as C } from "lucide-react";
9
+ import { jsx as w, jsxs as T } from "react/jsx-runtime";
10
+ import { Badge as E, Button as D, Card as O, CardContent as k, CardDescription as ee, CardHeader as A, CardTitle as j } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/brainyrich/components/AggregationPanel.tsx
12
- var k = {
12
+ var M = {
13
13
  ACTIVE: "default",
14
14
  PENDING: "secondary",
15
15
  ERROR: "destructive",
16
16
  REVOKED: "outline"
17
17
  };
18
- function A() {
19
- let A = r(), j = c(), M = s(), N = l(), P = u(), F = f(), I = d(), [L, R] = m(!1), [z, B] = m(null), V = j.data.find((e) => e.provider === "PLAID"), H = !!V?.configured, U = L || N.isPending || P.isPending, W = async () => {
20
- R(!0);
18
+ function N() {
19
+ let N = r(), P = c(), F = s(), I = u(), L = f(), R = m(), z = p(), B = d(), V = l(), [H, U] = g(!1), [W, G] = g(null), [K, q] = g(null), J = P.data.find((e) => e.provider === "PLAID"), Y = !!J?.configured, X = H || I.isPending || L.isPending, Z = async () => {
20
+ U(!0);
21
21
  try {
22
- let n = await N.mutateAsync().catch(() => null);
22
+ let n = await I.mutateAsync().catch(() => null);
23
23
  if (!n) return;
24
24
  let r;
25
25
  try {
26
- r = await p(n.linkToken);
26
+ r = await h(n.linkToken);
27
27
  } catch (n) {
28
28
  t.error(e(n));
29
29
  return;
30
30
  }
31
- r.kind === "success" ? await P.mutateAsync({
31
+ r.kind === "success" ? await L.mutateAsync({
32
32
  publicToken: r.publicToken,
33
33
  institutionId: r.institutionId,
34
34
  institutionName: r.institutionName
35
- }).catch(() => void 0) : r.message && t.info(A("brainyrich.aggregation.linkCancelled", "Linking was not completed"), r.message);
35
+ }).catch(() => void 0) : r.message && t.info(N("brainyrich.aggregation.linkCancelled", "Linking was not completed"), r.message);
36
36
  } finally {
37
- R(!1);
37
+ U(!1);
38
38
  }
39
- }, G = () => {
40
- z && I.mutate(z.id, { onSettled: () => B(null) });
41
- }, K = M.data;
42
- return /* @__PURE__ */ x(w, {
39
+ }, Q = async (n) => {
40
+ G(n.id);
41
+ try {
42
+ let r = await B.mutateAsync(n.id).catch(() => null);
43
+ if (!r) return;
44
+ let i;
45
+ try {
46
+ i = await h(r.linkToken);
47
+ } catch (n) {
48
+ t.error(e(n));
49
+ return;
50
+ }
51
+ i.kind === "success" ? await V.mutateAsync(n.id).catch(() => void 0) : i.message && t.info(N("brainyrich.aggregation.reconnectCancelled", "Reconnecting was not completed"), i.message);
52
+ } finally {
53
+ G(null);
54
+ }
55
+ }, te = () => {
56
+ K && z.mutate(K.id, { onSettled: () => q(null) });
57
+ }, $ = F.data;
58
+ return /* @__PURE__ */ T(O, {
43
59
  "data-testid": "brainyrich-aggregation-panel",
44
60
  children: [
45
- /* @__PURE__ */ x(D, {
61
+ /* @__PURE__ */ T(A, {
46
62
  className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",
47
- children: [/* @__PURE__ */ x("div", {
63
+ children: [/* @__PURE__ */ T("div", {
48
64
  className: "space-y-1.5",
49
- children: [/* @__PURE__ */ x(O, {
65
+ children: [/* @__PURE__ */ T(j, {
50
66
  className: "flex items-center gap-2",
51
67
  children: [
52
- /* @__PURE__ */ b(g, {
68
+ /* @__PURE__ */ w(b, {
53
69
  className: "h-5 w-5",
54
70
  "aria-hidden": "true"
55
71
  }),
56
- A("brainyrich.aggregation.title", "Automatic account linking"),
57
- H && V?.environment === "sandbox" ? /* @__PURE__ */ b(S, {
72
+ N("brainyrich.aggregation.title", "Automatic account linking"),
73
+ Y && J?.environment === "sandbox" ? /* @__PURE__ */ w(E, {
58
74
  variant: "secondary",
59
75
  "data-testid": "brainyrich-aggregation-sandbox-badge",
60
- children: A("brainyrich.aggregation.sandbox", "Sandbox")
76
+ children: N("brainyrich.aggregation.sandbox", "Sandbox")
61
77
  }) : null
62
78
  ]
63
- }), /* @__PURE__ */ b(E, { children: A("brainyrich.aggregation.description", "Connect a bank or brokerage through Plaid to sync balances and transactions automatically. You sign in on your bank’s own screen; BrainyRich never sees your bank password.") })]
64
- }), H ? /* @__PURE__ */ x(C, {
79
+ }), /* @__PURE__ */ w(ee, { children: N("brainyrich.aggregation.description", "Connect a bank or brokerage through Plaid to sync balances and transactions automatically. You sign in on your bank’s own screen; BrainyRich never sees your bank password.") })]
80
+ }), Y ? /* @__PURE__ */ T(D, {
65
81
  size: "sm",
66
- onClick: W,
67
- disabled: U,
68
- "aria-busy": U,
82
+ onClick: Z,
83
+ disabled: X,
84
+ "aria-busy": X,
69
85
  "data-testid": "brainyrich-aggregation-link-button",
70
- children: [/* @__PURE__ */ b(g, {
86
+ children: [/* @__PURE__ */ w(b, {
71
87
  className: "size-4",
72
88
  "aria-hidden": "true"
73
- }), U ? A("brainyrich.aggregation.linking", "Linking…") : A("brainyrich.aggregation.link", "Link an institution")]
89
+ }), X ? N("brainyrich.aggregation.linking", "Linking…") : N("brainyrich.aggregation.link", "Link an institution")]
74
90
  }) : null]
75
91
  }),
76
- /* @__PURE__ */ x(T, {
92
+ /* @__PURE__ */ T(k, {
77
93
  className: "space-y-4",
78
- children: [j.isLoading ? /* @__PURE__ */ b(a, { rows: 1 }) : j.isError ? /* @__PURE__ */ b(i, {
79
- description: A("brainyrich.aggregation.providersError", "We could not check whether automatic linking is available. Please try again."),
80
- onRetry: () => j.refetch()
81
- }) : H ? null : /* @__PURE__ */ x("div", {
94
+ children: [P.isLoading ? /* @__PURE__ */ w(a, { rows: 1 }) : P.isError ? /* @__PURE__ */ w(i, {
95
+ description: N("brainyrich.aggregation.providersError", "We could not check whether automatic linking is available. Please try again."),
96
+ onRetry: () => P.refetch()
97
+ }) : Y ? null : /* @__PURE__ */ T("div", {
82
98
  className: "flex items-start gap-3 rounded-lg border border-border-subtle bg-bg-sunken p-4 text-sm",
83
99
  "data-testid": "brainyrich-aggregation-unavailable",
84
- children: [/* @__PURE__ */ b(v, {
100
+ children: [/* @__PURE__ */ w(S, {
85
101
  className: "mt-0.5 size-4 shrink-0 text-text-secondary",
86
102
  "aria-hidden": "true"
87
- }), /* @__PURE__ */ b("p", {
103
+ }), /* @__PURE__ */ w("p", {
88
104
  className: "text-text-secondary",
89
- children: A("brainyrich.aggregation.unavailable", "Automatic linking is not switched on for this workspace yet. Add accounts with “New account” — balances and transactions you enter count everywhere synced data does.")
105
+ children: N("brainyrich.aggregation.unavailable", "Automatic linking is not switched on for this workspace yet. Add accounts with “New account” — balances and transactions you enter count everywhere synced data does.")
90
106
  })]
91
- }), M.isLoading ? null : M.isError ? /* @__PURE__ */ b(i, { onRetry: () => M.refetch() }) : K.length > 0 ? /* @__PURE__ */ b("ul", {
107
+ }), F.isLoading ? null : F.isError ? /* @__PURE__ */ w(i, { onRetry: () => F.refetch() }) : $.length > 0 ? /* @__PURE__ */ w("ul", {
92
108
  className: "divide-y divide-border-subtle rounded-lg border border-border-subtle",
93
109
  "data-testid": "brainyrich-aggregation-links",
94
- children: K.map((e) => {
95
- let t = e.status === "REVOKED", n = F.isPending && F.variables === e.id;
96
- return /* @__PURE__ */ x("li", {
110
+ children: $.map((e) => {
111
+ let t = e.status === "REVOKED", n = R.isPending && R.variables === e.id, r = !(e.status === "ERROR" && e.needsReauthentication);
112
+ return /* @__PURE__ */ T("li", {
97
113
  className: "flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between",
98
114
  "data-testid": "brainyrich-aggregation-link-row",
99
- children: [/* @__PURE__ */ x("div", {
115
+ children: [/* @__PURE__ */ T("div", {
100
116
  className: "flex min-w-0 items-start gap-3",
101
- children: [/* @__PURE__ */ b("div", {
117
+ children: [/* @__PURE__ */ w("div", {
102
118
  className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary",
103
- children: /* @__PURE__ */ b(h, {
119
+ children: /* @__PURE__ */ w(v, {
104
120
  className: "size-4",
105
121
  "aria-hidden": "true"
106
122
  })
107
- }), /* @__PURE__ */ x("div", {
123
+ }), /* @__PURE__ */ T("div", {
108
124
  className: "min-w-0 space-y-1",
109
125
  children: [
110
- /* @__PURE__ */ x("div", {
126
+ /* @__PURE__ */ T("div", {
111
127
  className: "flex flex-wrap items-center gap-2",
112
- children: [/* @__PURE__ */ b("span", {
128
+ children: [/* @__PURE__ */ w("span", {
113
129
  className: "font-medium text-text-primary",
114
- children: e.institutionName ?? A("brainyrich.aggregation.unnamed", "Linked institution")
115
- }), /* @__PURE__ */ b(S, {
116
- variant: k[e.status],
130
+ children: e.institutionName ?? N("brainyrich.aggregation.unnamed", "Linked institution")
131
+ }), /* @__PURE__ */ w(E, {
132
+ variant: M[e.status],
117
133
  children: e.status
118
134
  })]
119
135
  }),
120
- /* @__PURE__ */ b("p", {
136
+ /* @__PURE__ */ w("p", {
121
137
  className: "text-xs text-text-secondary",
122
- children: e.lastSyncedAt ? A("brainyrich.aggregation.lastSynced", "Last synced {{date}}", { date: o(e.lastSyncedAt) }) : A("brainyrich.aggregation.neverSynced", "Not synced yet")
138
+ children: e.lastSyncedAt ? N("brainyrich.aggregation.lastSynced", "Last synced {{date}}", { date: o(e.lastSyncedAt) }) : N("brainyrich.aggregation.neverSynced", "Not synced yet")
123
139
  }),
124
- e.status === "ERROR" && e.lastSyncError ? /* @__PURE__ */ b("p", {
140
+ !t && e.needsReauthentication ? /* @__PURE__ */ T("p", {
141
+ className: "flex items-start gap-1.5 text-xs text-status-warning-text",
142
+ "data-testid": "brainyrich-aggregation-reauth-notice",
143
+ children: [/* @__PURE__ */ w(_, {
144
+ className: "mt-0.5 size-3.5 shrink-0",
145
+ "aria-hidden": "true"
146
+ }), /* @__PURE__ */ w("span", { children: e.consentExpiresAt && e.status !== "ERROR" ? N("brainyrich.aggregation.consentExpiring", "Access to this institution expires on {{date}}. Reconnect to keep it syncing.", { date: o(e.consentExpiresAt) }) : N("brainyrich.aggregation.reauthRequired", "The institution needs you to sign in again before it can sync.") })]
147
+ }) : null,
148
+ e.status === "ERROR" && e.lastSyncError && !e.needsReauthentication ? /* @__PURE__ */ w("p", {
125
149
  className: "break-words text-xs text-status-error-text",
126
150
  children: e.lastSyncError
151
+ }) : null,
152
+ e.status === "ACTIVE" && e.nextSyncAt ? /* @__PURE__ */ w("p", {
153
+ className: "text-xs text-text-muted",
154
+ children: N("brainyrich.aggregation.nextSync", "Next automatic sync {{date}}", { date: o(e.nextSyncAt) })
127
155
  }) : null
128
156
  ]
129
157
  })]
130
- }), t ? null : /* @__PURE__ */ x("div", {
131
- className: "flex shrink-0 gap-2",
132
- children: [/* @__PURE__ */ x(C, {
133
- size: "sm",
134
- variant: "outline",
135
- onClick: () => F.mutate(e.id),
136
- disabled: F.isPending,
137
- "aria-busy": n,
138
- children: [/* @__PURE__ */ b(_, {
139
- className: "size-4",
140
- "aria-hidden": "true"
141
- }), n ? A("brainyrich.aggregation.syncing", "Syncing…") : A("brainyrich.aggregation.sync", "Sync now")]
142
- }), /* @__PURE__ */ x(C, {
143
- size: "sm",
144
- variant: "outline",
145
- onClick: () => B(e),
146
- children: [/* @__PURE__ */ b(y, {
147
- className: "size-4",
148
- "aria-hidden": "true"
149
- }), A("brainyrich.aggregation.disconnect", "Disconnect")]
150
- })]
158
+ }), t ? null : /* @__PURE__ */ T("div", {
159
+ className: "flex shrink-0 flex-wrap gap-2",
160
+ children: [
161
+ e.needsReauthentication ? /* @__PURE__ */ T(D, {
162
+ size: "sm",
163
+ onClick: () => void Q(e),
164
+ disabled: W !== null || !Y,
165
+ "aria-busy": W === e.id,
166
+ "data-testid": "brainyrich-aggregation-reconnect-button",
167
+ children: [/* @__PURE__ */ w(y, {
168
+ className: "size-4",
169
+ "aria-hidden": "true"
170
+ }), W === e.id ? N("brainyrich.aggregation.reconnecting", "Reconnecting…") : N("brainyrich.aggregation.reconnect", "Reconnect")]
171
+ }) : null,
172
+ r ? /* @__PURE__ */ T(D, {
173
+ size: "sm",
174
+ variant: "outline",
175
+ onClick: () => R.mutate(e.id),
176
+ disabled: R.isPending,
177
+ "aria-busy": n,
178
+ children: [/* @__PURE__ */ w(x, {
179
+ className: "size-4",
180
+ "aria-hidden": "true"
181
+ }), n ? N("brainyrich.aggregation.syncing", "Syncing…") : N("brainyrich.aggregation.sync", "Sync now")]
182
+ }) : null,
183
+ /* @__PURE__ */ T(D, {
184
+ size: "sm",
185
+ variant: "outline",
186
+ onClick: () => q(e),
187
+ children: [/* @__PURE__ */ w(C, {
188
+ className: "size-4",
189
+ "aria-hidden": "true"
190
+ }), N("brainyrich.aggregation.disconnect", "Disconnect")]
191
+ })
192
+ ]
151
193
  })]
152
194
  }, e.id);
153
195
  })
154
196
  }) : null]
155
197
  }),
156
- /* @__PURE__ */ b(n, {
157
- open: z !== null,
158
- onOpenChange: (e) => !e && B(null),
159
- title: A("brainyrich.aggregation.disconnectTitle", "Disconnect this institution?"),
160
- description: A("brainyrich.aggregation.disconnectDescription", "BrainyRich stops syncing and revokes its access at the provider. Accounts synced from it are marked closed; their history stays."),
161
- confirmLabel: A("brainyrich.aggregation.disconnect", "Disconnect"),
162
- pendingLabel: A("brainyrich.aggregation.disconnecting", "Disconnecting…"),
198
+ /* @__PURE__ */ w(n, {
199
+ open: K !== null,
200
+ onOpenChange: (e) => !e && q(null),
201
+ title: N("brainyrich.aggregation.disconnectTitle", "Disconnect this institution?"),
202
+ description: N("brainyrich.aggregation.disconnectDescription", "BrainyRich stops syncing and revokes its access at the provider. Accounts synced from it are marked closed; their history stays."),
203
+ confirmLabel: N("brainyrich.aggregation.disconnect", "Disconnect"),
204
+ pendingLabel: N("brainyrich.aggregation.disconnecting", "Disconnecting…"),
163
205
  destructive: !0,
164
- pending: I.isPending,
165
- onConfirm: G
206
+ pending: z.isPending,
207
+ onConfirm: te
166
208
  })
167
209
  ]
168
210
  });
169
211
  }
170
212
  //#endregion
171
- export { A as AggregationPanel };
213
+ export { N as AggregationPanel };
172
214
 
173
215
  //# sourceMappingURL=AggregationPanel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AggregationPanel.js","names":[],"sources":["../../../src/brainyrich/components/AggregationPanel.tsx"],"sourcesContent":["/**\n * AggregationPanel — \"Automatic account linking\" card on the Accounts page\n * (BOFF-4234).\n *\n * Offers \"Link an institution\" only when the backend reports a configured\n * provider (`braAggregationProviders`). Otherwise it says plainly that\n * accounts are added manually today, so the page never presents a button that\n * cannot work. Linked institutions are listed with their sync status and can\n * be re-synced or disconnected.\n */\nimport { useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { Building2, Link2, RefreshCw, ShieldCheck, Unlink } from 'lucide-react';\n\nimport {\n useAggregationLinks,\n useAggregationProviders,\n useCreatePlaidLinkToken,\n useExchangePlaidPublicToken,\n useRevokeAggregationLink,\n useSyncAggregationLink,\n} from '../hooks/useAggregation';\nimport { useTr } from '../hooks/useTr';\nimport type { BraAggregationLink, BraAggregationLinkStatus } from '../types/bra';\nimport { openPlaidLink, type PlaidLinkOutcome } from '../utils/plaidLink';\nimport { ErrorState, LoadingState, formatDate } from '../pages/_shared';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { errorMessage, notify } from './notify';\n\nconst STATUS_VARIANT: Record<\n BraAggregationLinkStatus,\n 'default' | 'secondary' | 'destructive' | 'outline'\n> = {\n ACTIVE: 'default',\n PENDING: 'secondary',\n ERROR: 'destructive',\n REVOKED: 'outline',\n};\n\nexport function AggregationPanel() {\n const tr = useTr();\n const providers = useAggregationProviders();\n const links = useAggregationLinks();\n const createToken = useCreatePlaidLinkToken();\n const exchange = useExchangePlaidPublicToken();\n const sync = useSyncAggregationLink();\n const revoke = useRevokeAggregationLink();\n\n const [linking, setLinking] = useState(false);\n const [revokeTarget, setRevokeTarget] = useState<BraAggregationLink | null>(null);\n\n const plaid = providers.data.find((p) => p.provider === 'PLAID');\n const canLink = Boolean(plaid?.configured);\n const busy = linking || createToken.isPending || exchange.isPending;\n\n const startLinking = async () => {\n setLinking(true);\n try {\n // Failures of the two backend calls are toasted by the hooks' own\n // feedback handlers, so they are only caught here to end the flow.\n const token = await createToken.mutateAsync().catch(() => null);\n if (!token) return;\n\n let outcome: PlaidLinkOutcome;\n try {\n outcome = await openPlaidLink(token.linkToken);\n } catch (err: unknown) {\n notify.error(errorMessage(err));\n return;\n }\n\n if (outcome.kind === 'success') {\n await exchange\n .mutateAsync({\n publicToken: outcome.publicToken,\n institutionId: outcome.institutionId,\n institutionName: outcome.institutionName,\n })\n .catch(() => undefined);\n } else if (outcome.message) {\n notify.info(\n tr('brainyrich.aggregation.linkCancelled', 'Linking was not completed'),\n outcome.message\n );\n }\n } finally {\n setLinking(false);\n }\n };\n\n const confirmRevoke = () => {\n if (!revokeTarget) return;\n revoke.mutate(revokeTarget.id, { onSettled: () => setRevokeTarget(null) });\n };\n\n const visibleLinks = links.data;\n\n return (\n <Card data-testid=\"brainyrich-aggregation-panel\">\n <CardHeader className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1.5\">\n <CardTitle className=\"flex items-center gap-2\">\n <Link2 className=\"h-5 w-5\" aria-hidden=\"true\" />\n {tr('brainyrich.aggregation.title', 'Automatic account linking')}\n {canLink && plaid?.environment === 'sandbox' ? (\n <Badge variant=\"secondary\" data-testid=\"brainyrich-aggregation-sandbox-badge\">\n {tr('brainyrich.aggregation.sandbox', 'Sandbox')}\n </Badge>\n ) : null}\n </CardTitle>\n <CardDescription>\n {tr(\n 'brainyrich.aggregation.description',\n 'Connect a bank or brokerage through Plaid to sync balances and transactions automatically. You sign in on your bank’s own screen; BrainyRich never sees your bank password.'\n )}\n </CardDescription>\n </div>\n {canLink ? (\n <Button\n size=\"sm\"\n onClick={startLinking}\n disabled={busy}\n aria-busy={busy}\n data-testid=\"brainyrich-aggregation-link-button\"\n >\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n {busy\n ? tr('brainyrich.aggregation.linking', 'Linking…')\n : tr('brainyrich.aggregation.link', 'Link an institution')}\n </Button>\n ) : null}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {providers.isLoading ? (\n <LoadingState rows={1} />\n ) : providers.isError ? (\n <ErrorState\n description={tr(\n 'brainyrich.aggregation.providersError',\n 'We could not check whether automatic linking is available. Please try again.'\n )}\n onRetry={() => providers.refetch()}\n />\n ) : !canLink ? (\n <div\n className=\"flex items-start gap-3 rounded-lg border border-border-subtle bg-bg-sunken p-4 text-sm\"\n data-testid=\"brainyrich-aggregation-unavailable\"\n >\n <ShieldCheck\n className=\"mt-0.5 size-4 shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <p className=\"text-text-secondary\">\n {tr(\n 'brainyrich.aggregation.unavailable',\n 'Automatic linking is not switched on for this workspace yet. Add accounts with “New account” — balances and transactions you enter count everywhere synced data does.'\n )}\n </p>\n </div>\n ) : null}\n\n {links.isLoading ? null : links.isError ? (\n <ErrorState onRetry={() => links.refetch()} />\n ) : visibleLinks.length > 0 ? (\n <ul\n className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle\"\n data-testid=\"brainyrich-aggregation-links\"\n >\n {visibleLinks.map((link) => {\n const revoked = link.status === 'REVOKED';\n const syncingThis = sync.isPending && sync.variables === link.id;\n return (\n <li\n key={link.id}\n className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\"\n data-testid=\"brainyrich-aggregation-link-row\"\n >\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <Building2 className=\"size-4\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-text-primary\">\n {link.institutionName ??\n tr('brainyrich.aggregation.unnamed', 'Linked institution')}\n </span>\n <Badge variant={STATUS_VARIANT[link.status]}>{link.status}</Badge>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {link.lastSyncedAt\n ? tr('brainyrich.aggregation.lastSynced', 'Last synced {{date}}', {\n date: formatDate(link.lastSyncedAt),\n })\n : tr('brainyrich.aggregation.neverSynced', 'Not synced yet')}\n </p>\n {link.status === 'ERROR' && link.lastSyncError ? (\n <p className=\"break-words text-xs text-status-error-text\">\n {link.lastSyncError}\n </p>\n ) : null}\n </div>\n </div>\n {revoked ? null : (\n <div className=\"flex shrink-0 gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => sync.mutate(link.id)}\n disabled={sync.isPending}\n aria-busy={syncingThis}\n >\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n {syncingThis\n ? tr('brainyrich.aggregation.syncing', 'Syncing…')\n : tr('brainyrich.aggregation.sync', 'Sync now')}\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRevokeTarget(link)}>\n <Unlink className=\"size-4\" aria-hidden=\"true\" />\n {tr('brainyrich.aggregation.disconnect', 'Disconnect')}\n </Button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n ) : null}\n </CardContent>\n\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => !open && setRevokeTarget(null)}\n title={tr('brainyrich.aggregation.disconnectTitle', 'Disconnect this institution?')}\n description={tr(\n 'brainyrich.aggregation.disconnectDescription',\n 'BrainyRich stops syncing and revokes its access at the provider. Accounts synced from it are marked closed; their history stays.'\n )}\n confirmLabel={tr('brainyrich.aggregation.disconnect', 'Disconnect')}\n pendingLabel={tr('brainyrich.aggregation.disconnecting', 'Disconnecting…')}\n destructive\n pending={revoke.isPending}\n onConfirm={confirmRevoke}\n />\n </Card>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AggregationPanel.js","names":[],"sources":["../../../src/brainyrich/components/AggregationPanel.tsx"],"sourcesContent":["/**\n * AggregationPanel — \"Automatic account linking\" card on the Accounts page\n * (BOFF-4234, BOFF-9677).\n *\n * Offers \"Link an institution\" only when the backend reports a configured\n * provider (`braAggregationProviders`). Otherwise it says plainly that\n * accounts are added manually today, so the page never presents a button that\n * cannot work. Linked institutions are listed with their sync status and can\n * be re-synced or disconnected. Links also re-sync on their own (Plaid\n * webhooks and a daily schedule); a link whose login broke or whose consent is\n * about to expire offers \"Reconnect\", which opens Plaid Link in update mode for\n * the same institution instead of creating a second link.\n */\nimport { useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n Building2,\n KeyRound,\n Link2,\n RefreshCw,\n ShieldCheck,\n Unlink,\n} from 'lucide-react';\n\nimport {\n useAggregationLinks,\n useAggregationProviders,\n useCompletePlaidReauthentication,\n useCreatePlaidLinkToken,\n useCreatePlaidUpdateLinkToken,\n useExchangePlaidPublicToken,\n useRevokeAggregationLink,\n useSyncAggregationLink,\n} from '../hooks/useAggregation';\nimport { useTr } from '../hooks/useTr';\nimport type { BraAggregationLink, BraAggregationLinkStatus } from '../types/bra';\nimport { openPlaidLink, type PlaidLinkOutcome } from '../utils/plaidLink';\nimport { ErrorState, LoadingState, formatDate } from '../pages/_shared';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { errorMessage, notify } from './notify';\n\nconst STATUS_VARIANT: Record<\n BraAggregationLinkStatus,\n 'default' | 'secondary' | 'destructive' | 'outline'\n> = {\n ACTIVE: 'default',\n PENDING: 'secondary',\n ERROR: 'destructive',\n REVOKED: 'outline',\n};\n\nexport function AggregationPanel() {\n const tr = useTr();\n const providers = useAggregationProviders();\n const links = useAggregationLinks();\n const createToken = useCreatePlaidLinkToken();\n const exchange = useExchangePlaidPublicToken();\n const sync = useSyncAggregationLink();\n const revoke = useRevokeAggregationLink();\n const createUpdateToken = useCreatePlaidUpdateLinkToken();\n const completeReauth = useCompletePlaidReauthentication();\n\n const [linking, setLinking] = useState(false);\n const [reconnectingId, setReconnectingId] = useState<string | null>(null);\n const [revokeTarget, setRevokeTarget] = useState<BraAggregationLink | null>(null);\n\n const plaid = providers.data.find((p) => p.provider === 'PLAID');\n const canLink = Boolean(plaid?.configured);\n const busy = linking || createToken.isPending || exchange.isPending;\n\n const startLinking = async () => {\n setLinking(true);\n try {\n // Failures of the two backend calls are toasted by the hooks' own\n // feedback handlers, so they are only caught here to end the flow.\n const token = await createToken.mutateAsync().catch(() => null);\n if (!token) return;\n\n let outcome: PlaidLinkOutcome;\n try {\n outcome = await openPlaidLink(token.linkToken);\n } catch (err: unknown) {\n notify.error(errorMessage(err));\n return;\n }\n\n if (outcome.kind === 'success') {\n await exchange\n .mutateAsync({\n publicToken: outcome.publicToken,\n institutionId: outcome.institutionId,\n institutionName: outcome.institutionName,\n })\n .catch(() => undefined);\n } else if (outcome.message) {\n notify.info(\n tr('brainyrich.aggregation.linkCancelled', 'Linking was not completed'),\n outcome.message\n );\n }\n } finally {\n setLinking(false);\n }\n };\n\n /** Plaid Link update mode: re-authenticate the same Item, then clear its error and re-sync. */\n const reconnect = async (link: BraAggregationLink) => {\n setReconnectingId(link.id);\n try {\n const token = await createUpdateToken.mutateAsync(link.id).catch(() => null);\n if (!token) return;\n\n let outcome: PlaidLinkOutcome;\n try {\n outcome = await openPlaidLink(token.linkToken);\n } catch (err: unknown) {\n notify.error(errorMessage(err));\n return;\n }\n\n if (outcome.kind === 'success') {\n // Update mode needs no token exchange: the existing access token is valid again.\n await completeReauth.mutateAsync(link.id).catch(() => undefined);\n } else if (outcome.message) {\n notify.info(\n tr('brainyrich.aggregation.reconnectCancelled', 'Reconnecting was not completed'),\n outcome.message\n );\n }\n } finally {\n setReconnectingId(null);\n }\n };\n\n const confirmRevoke = () => {\n if (!revokeTarget) return;\n revoke.mutate(revokeTarget.id, { onSettled: () => setRevokeTarget(null) });\n };\n\n const visibleLinks = links.data;\n\n return (\n <Card data-testid=\"brainyrich-aggregation-panel\">\n <CardHeader className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1.5\">\n <CardTitle className=\"flex items-center gap-2\">\n <Link2 className=\"h-5 w-5\" aria-hidden=\"true\" />\n {tr('brainyrich.aggregation.title', 'Automatic account linking')}\n {canLink && plaid?.environment === 'sandbox' ? (\n <Badge variant=\"secondary\" data-testid=\"brainyrich-aggregation-sandbox-badge\">\n {tr('brainyrich.aggregation.sandbox', 'Sandbox')}\n </Badge>\n ) : null}\n </CardTitle>\n <CardDescription>\n {tr(\n 'brainyrich.aggregation.description',\n 'Connect a bank or brokerage through Plaid to sync balances and transactions automatically. You sign in on your bank’s own screen; BrainyRich never sees your bank password.'\n )}\n </CardDescription>\n </div>\n {canLink ? (\n <Button\n size=\"sm\"\n onClick={startLinking}\n disabled={busy}\n aria-busy={busy}\n data-testid=\"brainyrich-aggregation-link-button\"\n >\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n {busy\n ? tr('brainyrich.aggregation.linking', 'Linking…')\n : tr('brainyrich.aggregation.link', 'Link an institution')}\n </Button>\n ) : null}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {providers.isLoading ? (\n <LoadingState rows={1} />\n ) : providers.isError ? (\n <ErrorState\n description={tr(\n 'brainyrich.aggregation.providersError',\n 'We could not check whether automatic linking is available. Please try again.'\n )}\n onRetry={() => providers.refetch()}\n />\n ) : !canLink ? (\n <div\n className=\"flex items-start gap-3 rounded-lg border border-border-subtle bg-bg-sunken p-4 text-sm\"\n data-testid=\"brainyrich-aggregation-unavailable\"\n >\n <ShieldCheck\n className=\"mt-0.5 size-4 shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <p className=\"text-text-secondary\">\n {tr(\n 'brainyrich.aggregation.unavailable',\n 'Automatic linking is not switched on for this workspace yet. Add accounts with “New account” — balances and transactions you enter count everywhere synced data does.'\n )}\n </p>\n </div>\n ) : null}\n\n {links.isLoading ? null : links.isError ? (\n <ErrorState onRetry={() => links.refetch()} />\n ) : visibleLinks.length > 0 ? (\n <ul\n className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle\"\n data-testid=\"brainyrich-aggregation-links\"\n >\n {visibleLinks.map((link) => {\n const revoked = link.status === 'REVOKED';\n const syncingThis = sync.isPending && sync.variables === link.id;\n // A broken login cannot be fixed by re-syncing; only Reconnect helps.\n const canSync = !(link.status === 'ERROR' && link.needsReauthentication);\n return (\n <li\n key={link.id}\n className=\"flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\"\n data-testid=\"brainyrich-aggregation-link-row\"\n >\n <div className=\"flex min-w-0 items-start gap-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n <Building2 className=\"size-4\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-text-primary\">\n {link.institutionName ??\n tr('brainyrich.aggregation.unnamed', 'Linked institution')}\n </span>\n <Badge variant={STATUS_VARIANT[link.status]}>{link.status}</Badge>\n </div>\n <p className=\"text-xs text-text-secondary\">\n {link.lastSyncedAt\n ? tr('brainyrich.aggregation.lastSynced', 'Last synced {{date}}', {\n date: formatDate(link.lastSyncedAt),\n })\n : tr('brainyrich.aggregation.neverSynced', 'Not synced yet')}\n </p>\n {!revoked && link.needsReauthentication ? (\n <p\n className=\"flex items-start gap-1.5 text-xs text-status-warning-text\"\n data-testid=\"brainyrich-aggregation-reauth-notice\"\n >\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>\n {link.consentExpiresAt && link.status !== 'ERROR'\n ? tr(\n 'brainyrich.aggregation.consentExpiring',\n 'Access to this institution expires on {{date}}. Reconnect to keep it syncing.',\n { date: formatDate(link.consentExpiresAt) }\n )\n : tr(\n 'brainyrich.aggregation.reauthRequired',\n 'The institution needs you to sign in again before it can sync.'\n )}\n </span>\n </p>\n ) : null}\n {link.status === 'ERROR' &&\n link.lastSyncError &&\n !link.needsReauthentication ? (\n <p className=\"break-words text-xs text-status-error-text\">\n {link.lastSyncError}\n </p>\n ) : null}\n {link.status === 'ACTIVE' && link.nextSyncAt ? (\n <p className=\"text-xs text-text-muted\">\n {tr('brainyrich.aggregation.nextSync', 'Next automatic sync {{date}}', {\n date: formatDate(link.nextSyncAt),\n })}\n </p>\n ) : null}\n </div>\n </div>\n {revoked ? null : (\n <div className=\"flex shrink-0 flex-wrap gap-2\">\n {link.needsReauthentication ? (\n <Button\n size=\"sm\"\n onClick={() => void reconnect(link)}\n disabled={reconnectingId !== null || !canLink}\n aria-busy={reconnectingId === link.id}\n data-testid=\"brainyrich-aggregation-reconnect-button\"\n >\n <KeyRound className=\"size-4\" aria-hidden=\"true\" />\n {reconnectingId === link.id\n ? tr('brainyrich.aggregation.reconnecting', 'Reconnecting…')\n : tr('brainyrich.aggregation.reconnect', 'Reconnect')}\n </Button>\n ) : null}\n {canSync ? (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => sync.mutate(link.id)}\n disabled={sync.isPending}\n aria-busy={syncingThis}\n >\n <RefreshCw className=\"size-4\" aria-hidden=\"true\" />\n {syncingThis\n ? tr('brainyrich.aggregation.syncing', 'Syncing…')\n : tr('brainyrich.aggregation.sync', 'Sync now')}\n </Button>\n ) : null}\n <Button size=\"sm\" variant=\"outline\" onClick={() => setRevokeTarget(link)}>\n <Unlink className=\"size-4\" aria-hidden=\"true\" />\n {tr('brainyrich.aggregation.disconnect', 'Disconnect')}\n </Button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n ) : null}\n </CardContent>\n\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => !open && setRevokeTarget(null)}\n title={tr('brainyrich.aggregation.disconnectTitle', 'Disconnect this institution?')}\n description={tr(\n 'brainyrich.aggregation.disconnectDescription',\n 'BrainyRich stops syncing and revokes its access at the provider. Accounts synced from it are marked closed; their history stays.'\n )}\n confirmLabel={tr('brainyrich.aggregation.disconnect', 'Disconnect')}\n pendingLabel={tr('brainyrich.aggregation.disconnecting', 'Disconnecting…')}\n destructive\n pending={revoke.isPending}\n onConfirm={confirmRevoke}\n />\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -329,6 +329,7 @@ export declare const useAggregationLinks: () => {
329
329
  };
330
330
  /** Mints a short-lived Plaid Link token. Imperative: only called when the user starts linking. */
331
331
  export declare const useCreatePlaidLinkToken: () => import('@tanstack/react-query').UseMutationResult<BraPlaidLinkToken, unknown, void, unknown>;
332
+ export declare function syncSummary(result: BraAggregationSyncResult): string;
332
333
  export interface ExchangePlaidPublicTokenVars {
333
334
  publicToken: string;
334
335
  institutionId: string | null;
@@ -338,4 +339,11 @@ export interface ExchangePlaidPublicTokenVars {
338
339
  export declare const useExchangePlaidPublicToken: () => import('@tanstack/react-query').UseMutationResult<BraAggregationSyncResult, unknown, ExchangePlaidPublicTokenVars, unknown>;
339
340
  export declare const useSyncAggregationLink: () => import('@tanstack/react-query').UseMutationResult<BraAggregationSyncResult, unknown, string, unknown>;
340
341
  export declare const useRevokeAggregationLink: () => import('@tanstack/react-query').UseMutationResult<boolean, unknown, string, unknown>;
342
+ /**
343
+ * Mints a Plaid Link token in update mode for an existing link, so the user
344
+ * re-authenticates that same institution instead of linking it twice.
345
+ */
346
+ export declare const useCreatePlaidUpdateLinkToken: () => import('@tanstack/react-query').UseMutationResult<BraPlaidLinkToken, unknown, string, unknown>;
347
+ /** Called once Plaid Link update mode succeeds: clears the link's error and re-syncs it. */
348
+ export declare const useCompletePlaidReauthentication: () => import('@tanstack/react-query').UseMutationResult<BraAggregationSyncResult, unknown, string, unknown>;
341
349
  //# sourceMappingURL=useAggregation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAggregation.d.ts","sourceRoot":"","sources":["../../../src/brainyrich/hooks/useAggregation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,kBAAkB,EAClB,4BAA4B,EAC5B,wBAAwB,EACxB,iBAAiB,EACjB,UAAU,EACX,MAAM,cAAc,CAAC;AAkBtB,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkBnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmB/B,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,oGAkBnC,CAAC;AAkBF,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,mIAuBvC,CAAC;AAEF,eAAO,MAAM,sBAAsB,6GAqBlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,4FAmBpC,CAAC"}
1
+ {"version":3,"file":"useAggregation.d.ts","sourceRoot":"","sources":["../../../src/brainyrich/hooks/useAggregation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,kBAAkB,EAClB,4BAA4B,EAC5B,wBAAwB,EACxB,iBAAiB,EACjB,UAAU,EACX,MAAM,cAAc,CAAC;AAoBtB,4FAA4F;AAC5F,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkBnC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmB/B,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,uBAAuB,oGAkBnC,CAAC;AAiBF,wBAAgB,WAAW,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAQpE;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,mIAuBvC,CAAC;AAEF,eAAO,MAAM,sBAAsB,6GAqBlC,CAAC;AAEF,eAAO,MAAM,wBAAwB,4FAmBpC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,sGAezC,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,gCAAgC,6GAqB5C,CAAC"}
@@ -13,9 +13,10 @@ var c = {
13
13
  "aggregation",
14
14
  "links"
15
15
  ]
16
- }, l = "\n id workspaceId ownerId provider status institutionId institutionName\n lastSyncedAt lastSyncError createdAt updatedAt\n", u = `
16
+ }, l = "\n id workspaceId ownerId provider status institutionId institutionName\n lastSyncedAt lastSyncError errorCode consentExpiresAt needsReauthentication nextSyncAt\n createdAt updatedAt\n", u = `
17
17
  link { ${l} }
18
18
  accountsSynced transactionsSynced addedTransactions modifiedTransactions removedTransactions
19
+ holdingsSynced
19
20
  `, d = () => {
20
21
  let { apiGatewayUrl: t } = e(), n = o({
21
22
  queryKey: c.providers,
@@ -50,13 +51,19 @@ var c = {
50
51
  function m() {
51
52
  let e = s();
52
53
  return () => {
53
- e.invalidateQueries({ queryKey: c.links }), e.invalidateQueries({ queryKey: t.accounts }), e.invalidateQueries({ queryKey: t.transactions }), e.invalidateQueries({ queryKey: t.netWorth });
54
+ e.invalidateQueries({ queryKey: c.links }), e.invalidateQueries({ queryKey: t.accounts }), e.invalidateQueries({ queryKey: t.holdings }), e.invalidateQueries({ queryKey: t.transactions }), e.invalidateQueries({ queryKey: t.netWorth });
54
55
  };
55
56
  }
56
- function h(e) {
57
- return `Synced ${`${e.accountsSynced} account${e.accountsSynced === 1 ? "" : "s"}`} and ${`${e.transactionsSynced} transaction${e.transactionsSynced === 1 ? "" : "s"}`}`;
57
+ function h(e, t) {
58
+ return `${e} ${t}${e === 1 ? "" : "s"}`;
58
59
  }
59
- var g = () => {
60
+ function g(e) {
61
+ let t = [h(e.accountsSynced, "account"), h(e.transactionsSynced, "transaction")];
62
+ e.holdingsSynced > 0 && t.push(h(e.holdingsSynced, "holding"));
63
+ let n = t.pop();
64
+ return `Synced ${t.length > 0 ? `${t.join(", ")} and ${n}` : n}`;
65
+ }
66
+ var _ = () => {
60
67
  let { apiGatewayUrl: t } = e();
61
68
  return a({
62
69
  mutationFn: async (e) => (await n(`mutation BraExchangePlaidPublicToken(
@@ -67,20 +74,20 @@ var g = () => {
67
74
  ) { ${u} }
68
75
  }`, { ...e }, t)).braExchangePlaidPublicToken,
69
76
  ...i({
70
- success: (e) => `Institution linked. ${h(e)}.`,
77
+ success: (e) => `Institution linked. ${g(e)}.`,
71
78
  onSuccess: m()
72
79
  })
73
80
  });
74
- }, _ = () => {
81
+ }, v = () => {
75
82
  let { apiGatewayUrl: t } = e(), r = m();
76
83
  return a({
77
84
  mutationFn: async (e) => (await n(`mutation BraSyncAggregationLink($id: ID!) {
78
85
  braSyncAggregationLink(id: $id) { ${u} }
79
86
  }`, { id: e }, t)).braSyncAggregationLink,
80
- ...i({ success: (e) => `${h(e)}.` }),
87
+ ...i({ success: (e) => `${g(e)}.` }),
81
88
  onSettled: () => r()
82
89
  });
83
- }, v = () => {
90
+ }, y = () => {
84
91
  let { apiGatewayUrl: t } = e();
85
92
  return a({
86
93
  mutationFn: async (e) => (await n("mutation BraRevokeAggregationLink($id: ID!) {\n braRevokeAggregationLink(id: $id)\n }", { id: e }, t)).braRevokeAggregationLink,
@@ -89,8 +96,23 @@ var g = () => {
89
96
  onSuccess: m()
90
97
  })
91
98
  });
99
+ }, b = () => {
100
+ let { apiGatewayUrl: t } = e();
101
+ return a({
102
+ mutationFn: async (e) => (await n("mutation BraCreatePlaidUpdateLinkToken($id: ID!) {\n braCreatePlaidUpdateLinkToken(id: $id) { linkToken expiration requestId }\n }", { id: e }, t)).braCreatePlaidUpdateLinkToken,
103
+ ...i({})
104
+ });
105
+ }, x = () => {
106
+ let { apiGatewayUrl: t } = e(), r = m();
107
+ return a({
108
+ mutationFn: async (e) => (await n(`mutation BraCompletePlaidReauthentication($id: ID!) {
109
+ braCompletePlaidReauthentication(id: $id) { ${u} }
110
+ }`, { id: e }, t)).braCompletePlaidReauthentication,
111
+ ...i({ success: (e) => `Institution reconnected. ${g(e)}.` }),
112
+ onSettled: () => r()
113
+ });
92
114
  };
93
115
  //#endregion
94
- export { f as useAggregationLinks, d as useAggregationProviders, p as useCreatePlaidLinkToken, g as useExchangePlaidPublicToken, v as useRevokeAggregationLink, _ as useSyncAggregationLink };
116
+ export { f as useAggregationLinks, d as useAggregationProviders, x as useCompletePlaidReauthentication, p as useCreatePlaidLinkToken, b as useCreatePlaidUpdateLinkToken, _ as useExchangePlaidPublicToken, y as useRevokeAggregationLink, v as useSyncAggregationLink };
95
117
 
96
118
  //# sourceMappingURL=useAggregation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAggregation.js","names":[],"sources":["../../../src/brainyrich/hooks/useAggregation.ts"],"sourcesContent":["/**\n * Automatic account aggregation hooks (BOFF-4234).\n *\n * Wired to the `wspace-brainyrich-svc` aggregation module\n * (`src/modules/aggregation/schema/aggregation.gql`). Synced accounts land in\n * the same `BraAccount`/`BraTransaction` rows as manual entry, so every\n * successful link/sync/revoke also invalidates the accounts, transactions and\n * net-worth caches.\n */\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBrainyRich } from '../providers/BrainyRichProvider';\nimport type {\n BraAggregationLink,\n BraAggregationProviderStatus,\n BraAggregationSyncResult,\n BraPlaidLinkToken,\n Connection,\n} from '../types/bra';\nimport { KEY, braMutate, braQuery, mutationFeedback } from './useBrainyRichApi';\n\nconst AGG_KEY = {\n providers: ['brainyrich', 'aggregation', 'providers'] as const,\n links: ['brainyrich', 'aggregation', 'links'] as const,\n};\n\nconst LINK_FIELDS = `\n id workspaceId ownerId provider status institutionId institutionName\n lastSyncedAt lastSyncError createdAt updatedAt\n`;\n\nconst SYNC_RESULT_FIELDS = `\n link { ${LINK_FIELDS} }\n accountsSynced transactionsSynced addedTransactions modifiedTransactions removedTransactions\n`;\n\n/** Provider availability for this deployment — drives whether linking is offered at all. */\nexport const useAggregationProviders = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const query = useQuery({\n queryKey: AGG_KEY.providers,\n // Configuration only changes on a deploy; no need to refetch per mount.\n staleTime: 5 * 60_000,\n queryFn: async () => {\n const result = await braQuery<{ braAggregationProviders: BraAggregationProviderStatus[] }>(\n `query BraAggregationProviders {\n braAggregationProviders { provider configured environment countryCodes }\n }`,\n {},\n apiGatewayUrl\n );\n return result.braAggregationProviders;\n },\n });\n return { ...query, data: query.data ?? [] };\n};\n\n/** The caller's own linked institutions (links are personal, never shared). */\nexport const useAggregationLinks = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const query = useQuery({\n queryKey: AGG_KEY.links,\n queryFn: async () => {\n const result = await braQuery<{ braListAggregationLinks: Connection<BraAggregationLink> }>(\n `query BraListAggregationLinks($pagination: PaginationInput) {\n braListAggregationLinks(pagination: $pagination) {\n items { ${LINK_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { pagination: { limit: 50 } },\n apiGatewayUrl\n );\n return result.braListAggregationLinks;\n },\n });\n return { ...query, data: query.data?.items ?? [] };\n};\n\n/** Mints a short-lived Plaid Link token. Imperative: only called when the user starts linking. */\nexport const useCreatePlaidLinkToken = () => {\n const { apiGatewayUrl } = useBrainyRich();\n return useMutation({\n mutationFn: async () => {\n // `braCreatePlaidLinkToken` is a Query field on the schema; it is\n // issued through the mutation transport so it never hits the read cache\n // (every Link session needs a fresh token).\n const result = await braMutate<{ braCreatePlaidLinkToken: BraPlaidLinkToken }>(\n `query BraCreatePlaidLinkToken {\n braCreatePlaidLinkToken { linkToken expiration requestId }\n }`,\n {},\n apiGatewayUrl\n );\n return result.braCreatePlaidLinkToken;\n },\n ...mutationFeedback<BraPlaidLinkToken, void>({}),\n });\n};\n\nfunction useInvalidateSyncedData() {\n const qc = useQueryClient();\n return () => {\n qc.invalidateQueries({ queryKey: AGG_KEY.links });\n qc.invalidateQueries({ queryKey: KEY.accounts });\n qc.invalidateQueries({ queryKey: KEY.transactions });\n qc.invalidateQueries({ queryKey: KEY.netWorth });\n };\n}\n\nfunction syncSummary(result: BraAggregationSyncResult): string {\n const accounts = `${result.accountsSynced} account${result.accountsSynced === 1 ? '' : 's'}`;\n const txns = `${result.transactionsSynced} transaction${result.transactionsSynced === 1 ? '' : 's'}`;\n return `Synced ${accounts} and ${txns}`;\n}\n\nexport interface ExchangePlaidPublicTokenVars {\n publicToken: string;\n institutionId: string | null;\n institutionName: string | null;\n}\n\n/** Exchanges the Plaid Link `public_token` server-side and runs the first sync. */\nexport const useExchangePlaidPublicToken = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (vars: ExchangePlaidPublicTokenVars) => {\n const result = await braMutate<{ braExchangePlaidPublicToken: BraAggregationSyncResult }>(\n `mutation BraExchangePlaidPublicToken(\n $publicToken: String!, $institutionId: String, $institutionName: String\n ) {\n braExchangePlaidPublicToken(\n publicToken: $publicToken, institutionId: $institutionId, institutionName: $institutionName\n ) { ${SYNC_RESULT_FIELDS} }\n }`,\n { ...vars },\n apiGatewayUrl\n );\n return result.braExchangePlaidPublicToken;\n },\n ...mutationFeedback<BraAggregationSyncResult, ExchangePlaidPublicTokenVars>({\n success: (data) => `Institution linked. ${syncSummary(data)}.`,\n onSuccess: invalidate,\n }),\n });\n};\n\nexport const useSyncAggregationLink = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{ braSyncAggregationLink: BraAggregationSyncResult }>(\n `mutation BraSyncAggregationLink($id: ID!) {\n braSyncAggregationLink(id: $id) { ${SYNC_RESULT_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braSyncAggregationLink;\n },\n ...mutationFeedback<BraAggregationSyncResult, string>({\n success: (data) => `${syncSummary(data)}.`,\n }),\n // Invalidate on success AND failure: a failed sync flips the link to ERROR server-side; refresh so the\n // status badge and error text reflect it.\n onSettled: () => invalidate(),\n });\n};\n\nexport const useRevokeAggregationLink = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{ braRevokeAggregationLink: boolean }>(\n `mutation BraRevokeAggregationLink($id: ID!) {\n braRevokeAggregationLink(id: $id)\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braRevokeAggregationLink;\n },\n ...mutationFeedback<boolean, string>({\n success: 'Institution disconnected. Its synced accounts are now closed.',\n onSuccess: invalidate,\n }),\n });\n};\n"],"mappings":";;;;AAqBA,IAAM,IAAU;CACd,WAAW;EAAC;EAAc;EAAe;CAAW;CACpD,OAAO;EAAC;EAAc;EAAe;CAAO;AAC9C,GAEM,IAAc,gIAKd,IAAqB;WAChB,EAAY;;GAKV,UAAgC;CAC3C,IAAM,EAAE,qBAAkB,EAAc,GAClC,IAAQ,EAAS;EACrB,UAAU,EAAQ;EAElB,WAAW,IAAI;EACf,SAAS,aAQA,MAPc,EACnB,kIAGA,CAAC,GACD,CACF,GACc;CAElB,CAAC;CACD,OAAO;EAAE,GAAG;EAAO,MAAM,EAAM,QAAQ,CAAC;CAAE;AAC5C,GAGa,UAA4B;CACvC,IAAM,EAAE,qBAAkB,EAAc,GAClC,IAAQ,EAAS;EACrB,UAAU,EAAQ;EAClB,SAAS,aAWA,MAVc,EACnB;;sBAEc,EAAY;;;YAI1B,EAAE,YAAY,EAAE,OAAO,GAAG,EAAE,GAC5B,CACF,GACc;CAElB,CAAC;CACD,OAAO;EAAE,GAAG;EAAO,MAAM,EAAM,MAAM,SAAS,CAAC;CAAE;AACnD,GAGa,UAAgC;CAC3C,IAAM,EAAE,qBAAkB,EAAc;CACxC,OAAO,EAAY;EACjB,YAAY,aAWH,MAPc,EACnB,oHAGA,CAAC,GACD,CACF,GACc;EAEhB,GAAG,EAA0C,CAAC,CAAC;CACjD,CAAC;AACH;AAEA,SAAS,IAA0B;CACjC,IAAM,IAAK,EAAe;CAC1B,aAAa;EAIX,AAHA,EAAG,kBAAkB,EAAE,UAAU,EAAQ,MAAM,CAAC,GAChD,EAAG,kBAAkB,EAAE,UAAU,EAAI,SAAS,CAAC,GAC/C,EAAG,kBAAkB,EAAE,UAAU,EAAI,aAAa,CAAC,GACnD,EAAG,kBAAkB,EAAE,UAAU,EAAI,SAAS,CAAC;CACjD;AACF;AAEA,SAAS,EAAY,GAA0C;CAG7D,OAAO,UAAU,GAFG,EAAO,eAAe,UAAU,EAAO,mBAAmB,IAAI,KAAK,MAE7D,OAAO,GADjB,EAAO,mBAAmB,cAAc,EAAO,uBAAuB,IAAI,KAAK;AAEjG;AASA,IAAa,UAAoC;CAC/C,IAAM,EAAE,qBAAkB,EAAc;CAExC,OAAO,EAAY;EACjB,YAAY,OAAO,OAYV,MAXc,EACnB;;;;;gBAKQ,EAAmB;YAE3B,EAAE,GAAG,EAAK,GACV,CACF,GACc;EAEhB,GAAG,EAAyE;GAC1E,UAAU,MAAS,uBAAuB,EAAY,CAAI,EAAE;GAC5D,WAlBe,EAkBJ;EACb,CAAC;CACH,CAAC;AACH,GAEa,UAA+B;CAC1C,IAAM,EAAE,qBAAkB,EAAc,GAClC,IAAa,EAAwB;CAC3C,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPc,EACnB;8CACsC,EAAmB;YAEzD,EAAE,MAAG,GACL,CACF,GACc;EAEhB,GAAG,EAAmD,EACpD,UAAU,MAAS,GAAG,EAAY,CAAI,EAAE,GAC1C,CAAC;EAGD,iBAAiB,EAAW;CAC9B,CAAC;AACH,GAEa,UAAiC;CAC5C,IAAM,EAAE,qBAAkB,EAAc;CAExC,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPc,EACnB,yGAGA,EAAE,MAAG,GACL,CACF,GACc;EAEhB,GAAG,EAAkC;GACnC,SAAS;GACT,WAde,EAcJ;EACb,CAAC;CACH,CAAC;AACH"}
1
+ {"version":3,"file":"useAggregation.js","names":[],"sources":["../../../src/brainyrich/hooks/useAggregation.ts"],"sourcesContent":["/**\n * Automatic account aggregation hooks (BOFF-4234, BOFF-9677).\n *\n * Wired to the `wspace-brainyrich-svc` aggregation module\n * (`src/modules/aggregation/schema/aggregation.gql`). Synced accounts land in\n * the same `BraAccount`/`BraTransaction` rows as manual entry, so every\n * successful link/sync/revoke also invalidates the accounts, transactions and\n * net-worth caches.\n */\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBrainyRich } from '../providers/BrainyRichProvider';\nimport type {\n BraAggregationLink,\n BraAggregationProviderStatus,\n BraAggregationSyncResult,\n BraPlaidLinkToken,\n Connection,\n} from '../types/bra';\nimport { KEY, braMutate, braQuery, mutationFeedback } from './useBrainyRichApi';\n\nconst AGG_KEY = {\n providers: ['brainyrich', 'aggregation', 'providers'] as const,\n links: ['brainyrich', 'aggregation', 'links'] as const,\n};\n\nconst LINK_FIELDS = `\n id workspaceId ownerId provider status institutionId institutionName\n lastSyncedAt lastSyncError errorCode consentExpiresAt needsReauthentication nextSyncAt\n createdAt updatedAt\n`;\n\nconst SYNC_RESULT_FIELDS = `\n link { ${LINK_FIELDS} }\n accountsSynced transactionsSynced addedTransactions modifiedTransactions removedTransactions\n holdingsSynced\n`;\n\n/** Provider availability for this deployment — drives whether linking is offered at all. */\nexport const useAggregationProviders = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const query = useQuery({\n queryKey: AGG_KEY.providers,\n // Configuration only changes on a deploy; no need to refetch per mount.\n staleTime: 5 * 60_000,\n queryFn: async () => {\n const result = await braQuery<{ braAggregationProviders: BraAggregationProviderStatus[] }>(\n `query BraAggregationProviders {\n braAggregationProviders { provider configured environment countryCodes }\n }`,\n {},\n apiGatewayUrl\n );\n return result.braAggregationProviders;\n },\n });\n return { ...query, data: query.data ?? [] };\n};\n\n/** The caller's own linked institutions (links are personal, never shared). */\nexport const useAggregationLinks = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const query = useQuery({\n queryKey: AGG_KEY.links,\n queryFn: async () => {\n const result = await braQuery<{ braListAggregationLinks: Connection<BraAggregationLink> }>(\n `query BraListAggregationLinks($pagination: PaginationInput) {\n braListAggregationLinks(pagination: $pagination) {\n items { ${LINK_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { pagination: { limit: 50 } },\n apiGatewayUrl\n );\n return result.braListAggregationLinks;\n },\n });\n return { ...query, data: query.data?.items ?? [] };\n};\n\n/** Mints a short-lived Plaid Link token. Imperative: only called when the user starts linking. */\nexport const useCreatePlaidLinkToken = () => {\n const { apiGatewayUrl } = useBrainyRich();\n return useMutation({\n mutationFn: async () => {\n // `braCreatePlaidLinkToken` is a Query field on the schema; it is\n // issued through the mutation transport so it never hits the read cache\n // (every Link session needs a fresh token).\n const result = await braMutate<{ braCreatePlaidLinkToken: BraPlaidLinkToken }>(\n `query BraCreatePlaidLinkToken {\n braCreatePlaidLinkToken { linkToken expiration requestId }\n }`,\n {},\n apiGatewayUrl\n );\n return result.braCreatePlaidLinkToken;\n },\n ...mutationFeedback<BraPlaidLinkToken, void>({}),\n });\n};\n\nfunction useInvalidateSyncedData() {\n const qc = useQueryClient();\n return () => {\n qc.invalidateQueries({ queryKey: AGG_KEY.links });\n qc.invalidateQueries({ queryKey: KEY.accounts });\n qc.invalidateQueries({ queryKey: KEY.holdings });\n qc.invalidateQueries({ queryKey: KEY.transactions });\n qc.invalidateQueries({ queryKey: KEY.netWorth });\n };\n}\n\nfunction plural(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? '' : 's'}`;\n}\n\nexport function syncSummary(result: BraAggregationSyncResult): string {\n const parts = [\n plural(result.accountsSynced, 'account'),\n plural(result.transactionsSynced, 'transaction'),\n ];\n if (result.holdingsSynced > 0) parts.push(plural(result.holdingsSynced, 'holding'));\n const last = parts.pop();\n return `Synced ${parts.length > 0 ? `${parts.join(', ')} and ${last}` : last}`;\n}\n\nexport interface ExchangePlaidPublicTokenVars {\n publicToken: string;\n institutionId: string | null;\n institutionName: string | null;\n}\n\n/** Exchanges the Plaid Link `public_token` server-side and runs the first sync. */\nexport const useExchangePlaidPublicToken = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (vars: ExchangePlaidPublicTokenVars) => {\n const result = await braMutate<{ braExchangePlaidPublicToken: BraAggregationSyncResult }>(\n `mutation BraExchangePlaidPublicToken(\n $publicToken: String!, $institutionId: String, $institutionName: String\n ) {\n braExchangePlaidPublicToken(\n publicToken: $publicToken, institutionId: $institutionId, institutionName: $institutionName\n ) { ${SYNC_RESULT_FIELDS} }\n }`,\n { ...vars },\n apiGatewayUrl\n );\n return result.braExchangePlaidPublicToken;\n },\n ...mutationFeedback<BraAggregationSyncResult, ExchangePlaidPublicTokenVars>({\n success: (data) => `Institution linked. ${syncSummary(data)}.`,\n onSuccess: invalidate,\n }),\n });\n};\n\nexport const useSyncAggregationLink = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{ braSyncAggregationLink: BraAggregationSyncResult }>(\n `mutation BraSyncAggregationLink($id: ID!) {\n braSyncAggregationLink(id: $id) { ${SYNC_RESULT_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braSyncAggregationLink;\n },\n ...mutationFeedback<BraAggregationSyncResult, string>({\n success: (data) => `${syncSummary(data)}.`,\n }),\n // Invalidate on success AND failure: a failed sync flips the link to ERROR server-side; refresh so the\n // status badge and error text reflect it.\n onSettled: () => invalidate(),\n });\n};\n\nexport const useRevokeAggregationLink = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{ braRevokeAggregationLink: boolean }>(\n `mutation BraRevokeAggregationLink($id: ID!) {\n braRevokeAggregationLink(id: $id)\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braRevokeAggregationLink;\n },\n ...mutationFeedback<boolean, string>({\n success: 'Institution disconnected. Its synced accounts are now closed.',\n onSuccess: invalidate,\n }),\n });\n};\n\n/**\n * Mints a Plaid Link token in update mode for an existing link, so the user\n * re-authenticates that same institution instead of linking it twice.\n */\nexport const useCreatePlaidUpdateLinkToken = () => {\n const { apiGatewayUrl } = useBrainyRich();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{ braCreatePlaidUpdateLinkToken: BraPlaidLinkToken }>(\n `mutation BraCreatePlaidUpdateLinkToken($id: ID!) {\n braCreatePlaidUpdateLinkToken(id: $id) { linkToken expiration requestId }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braCreatePlaidUpdateLinkToken;\n },\n ...mutationFeedback<BraPlaidLinkToken, string>({}),\n });\n};\n\n/** Called once Plaid Link update mode succeeds: clears the link's error and re-syncs it. */\nexport const useCompletePlaidReauthentication = () => {\n const { apiGatewayUrl } = useBrainyRich();\n const invalidate = useInvalidateSyncedData();\n return useMutation({\n mutationFn: async (id: string) => {\n const result = await braMutate<{\n braCompletePlaidReauthentication: BraAggregationSyncResult;\n }>(\n `mutation BraCompletePlaidReauthentication($id: ID!) {\n braCompletePlaidReauthentication(id: $id) { ${SYNC_RESULT_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return result.braCompletePlaidReauthentication;\n },\n ...mutationFeedback<BraAggregationSyncResult, string>({\n success: (data) => `Institution reconnected. ${syncSummary(data)}.`,\n }),\n onSettled: () => invalidate(),\n });\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Connection, BraNetWorthSummary, BraAccount, BraAccountStatus, BraPortfolio, BraPortfolioStatus, BraHolding, BraModelPortfolio, BraGoal, BraGoalStatus, BraGoalFunding, BraHousehold, BraHouseholdStatus, BraInsight, BraInsightSeverity, BraInsightStatus, BraTransaction, BraTransactionType, BraRebalanceProposal, BraRebalanceStatus, BraPortfolioRiskReport, BraRiskSummary, BraTaxLossReport, BraTaxLossThresholds, BraAdvisor, BraAdvisorRole, BraFirmReport, BraAdminFirmSummary, BraAdvisorBranding, BraUpsertAdvisorBrandingInput, BraClientPortalSummary, BraScheduledReport, BraReportRun, BraCreateScheduledReportInput, BraGenerateReportNowInput, BraMarketQuote, BraHoldingPriceRefreshResult, BraTradeInstruction, BraTradeInstructionStatus, BraCreateTradeInstructionInput, BraCreateAccountInput, BraUpdateAccountInput, BraCreatePortfolioInput, BraUpdatePortfolioInput, BraCreateHoldingInput, BraUpdateHoldingInput, BraCreateModelPortfolioInput, BraCreateGoalInput, BraUpdateGoalInput, BraLinkGoalFundingInput, BraCreateHouseholdInput, BraUpdateHouseholdInput, BraCreateInsightInput, BraCreateTransactionInput, BraCreateRebalanceProposalInput, PaginationInput } from '../types/bra';
1
+ import { Connection, BraNetWorthSummary, BraAccount, BraAccountStatus, BraPortfolio, BraPortfolioStatus, BraHolding, BraModelPortfolio, BraGoal, BraGoalStatus, BraGoalFunding, BraHousehold, BraHouseholdStatus, BraInsight, BraInsightSeverity, BraInsightStatus, BraTransaction, BraTransactionType, BraRebalanceProposal, BraRebalanceStatus, BraPortfolioRiskReport, BraRiskSummary, BraTaxLossReport, BraTaxLossThresholds, BraAdvisor, BraAdvisorRole, BraAdvisorStatus, BraFirmReport, BraAdminFirmSummary, BraAdvisorBranding, BraUpsertAdvisorBrandingInput, BraClientPortalSummary, BraScheduledReport, BraReportRun, BraCreateScheduledReportInput, BraGenerateReportNowInput, BraMarketQuote, BraHoldingPriceRefreshResult, BraTradeInstruction, BraTradeInstructionStatus, BraCreateTradeInstructionInput, BraCreateAccountInput, BraUpdateAccountInput, BraCreatePortfolioInput, BraUpdatePortfolioInput, BraCreateHoldingInput, BraUpdateHoldingInput, BraCreateModelPortfolioInput, BraCreateGoalInput, BraUpdateGoalInput, BraLinkGoalFundingInput, BraCreateHouseholdInput, BraUpdateHouseholdInput, BraCreateInsightInput, BraCreateTransactionInput, BraCreateRebalanceProposalInput, PaginationInput } from '../types/bra';
2
2
  export declare const braQuery: <TData>(query: string, variables: Record<string, unknown>, baseUrl?: string, opts?: {
3
3
  suppressGlobalError?: boolean;
4
4
  }) => Promise<TData>;
@@ -2583,6 +2583,15 @@ export declare const useSetAdvisorRole: () => import('@tanstack/react-query').Us
2583
2583
  userId: string;
2584
2584
  role: BraAdvisorRole;
2585
2585
  }, unknown>;
2586
+ /**
2587
+ * Activate or deactivate a roster row (BOFF-9890). FIRM_ADMIN only.
2588
+ * Deactivating frees the advisor's seat; reactivating takes one and is
2589
+ * refused with QUOTA_EXHAUSTED when every seat on the plan is in use.
2590
+ */
2591
+ export declare const useSetAdvisorStatus: () => import('@tanstack/react-query').UseMutationResult<BraAdvisor, unknown, {
2592
+ userId: string;
2593
+ status: BraAdvisorStatus;
2594
+ }, unknown>;
2586
2595
  /** Firm-wide aggregate AUM + per-advisor breakdown. FIRM_ADMIN only. */
2587
2596
  export declare const useFirmReport: () => import('@tanstack/react-query').UseQueryResult<BraFirmReport, Error>;
2588
2597
  /** Admin firm supervision dashboard: AUM, advisors, and household status breakdown. FIRM_ADMIN only. */