@cosmoops/connect-sdk 0.40.0 → 0.42.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/cjs/client/components/access-gate.js +18 -40
  3. package/dist/cjs/client/components/apps-grid.js +11 -3
  4. package/dist/cjs/client/components/ask-ai-errors.js +109 -0
  5. package/dist/cjs/client/components/ask-ai-turns.js +12 -0
  6. package/dist/cjs/client/components/ask-ai.js +75 -8
  7. package/dist/cjs/client/components/call-button.js +398 -0
  8. package/dist/cjs/client/components/plans.js +8 -2
  9. package/dist/cjs/client/components/readiness.js +2 -1
  10. package/dist/cjs/client/components/subscription.js +4 -4
  11. package/dist/cjs/client/components/user-menu.js +16 -11
  12. package/dist/cjs/client/handoff.js +78 -0
  13. package/dist/cjs/client/index.js +15 -7
  14. package/dist/cjs/client/portal-links.js +1 -1
  15. package/dist/cjs/client/provider.js +75 -4
  16. package/dist/cjs/client/use-checkout.js +23 -16
  17. package/dist/cjs/server/client.js +5 -1
  18. package/dist/cjs/server/handler.js +77 -1
  19. package/dist/cjs/server/resources/calls.js +58 -0
  20. package/dist/cjs/server/resources/identity.js +12 -0
  21. package/dist/cjs/server/resources/whatsapp.js +58 -0
  22. package/dist/esm/client/components/access-gate.js +18 -40
  23. package/dist/esm/client/components/apps-grid.js +11 -3
  24. package/dist/esm/client/components/ask-ai-errors.js +102 -0
  25. package/dist/esm/client/components/ask-ai-turns.js +12 -0
  26. package/dist/esm/client/components/ask-ai.js +76 -9
  27. package/dist/esm/client/components/call-button.js +358 -0
  28. package/dist/esm/client/components/plans.js +8 -2
  29. package/dist/esm/client/components/readiness.js +2 -1
  30. package/dist/esm/client/components/subscription.js +4 -4
  31. package/dist/esm/client/components/user-menu.js +16 -11
  32. package/dist/esm/client/handoff.js +72 -0
  33. package/dist/esm/client/index.js +6 -4
  34. package/dist/esm/client/portal-links.js +1 -1
  35. package/dist/esm/client/provider.js +75 -4
  36. package/dist/esm/client/use-checkout.js +23 -16
  37. package/dist/esm/server/client.js +5 -1
  38. package/dist/esm/server/handler.js +77 -1
  39. package/dist/esm/server/resources/calls.js +58 -0
  40. package/dist/esm/server/resources/identity.js +12 -0
  41. package/dist/esm/server/resources/whatsapp.js +54 -0
  42. package/dist/types/client/components/ask-ai-errors.d.ts +41 -0
  43. package/dist/types/client/components/ask-ai-turns.d.ts +5 -0
  44. package/dist/types/client/components/call-button.d.ts +132 -0
  45. package/dist/types/client/handoff.d.ts +62 -0
  46. package/dist/types/client/index.d.ts +5 -1
  47. package/dist/types/client/portal-links.d.ts +1 -1
  48. package/dist/types/client/provider.d.ts +72 -5
  49. package/dist/types/client/use-checkout.d.ts +4 -1
  50. package/dist/types/server/client.d.ts +4 -1
  51. package/dist/types/server/resources/calls.d.ts +32 -1
  52. package/dist/types/server/resources/identity.d.ts +10 -0
  53. package/dist/types/server/resources/whatsapp.d.ts +44 -0
  54. package/dist/types/types.d.ts +152 -1
  55. package/package.json +4 -1
@@ -0,0 +1,398 @@
1
+ "use client";
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.useConnectCall = useConnectCall;
38
+ exports.ConnectCallProvider = ConnectCallProvider;
39
+ exports.ConnectCallButton = ConnectCallButton;
40
+ exports.ConnectCallingStatus = ConnectCallingStatus;
41
+ exports.ConnectActiveCallBar = ConnectActiveCallBar;
42
+ const jsx_runtime_1 = require("react/jsx-runtime");
43
+ const react_1 = require("react");
44
+ const react_2 = require("@heroui/react");
45
+ const lucide_react_1 = require("lucide-react");
46
+ const cn_js_1 = require("../../cn.js");
47
+ const provider_js_1 = require("../provider.js");
48
+ const portal_links_js_1 = require("../portal-links.js");
49
+ const ConnectCallContext = (0, react_1.createContext)(null);
50
+ /** Start a call yourself. Must be inside `<ConnectCallProvider>`. */
51
+ function useConnectCall() {
52
+ const value = (0, react_1.useContext)(ConnectCallContext);
53
+ if (!value) {
54
+ throw new Error("useConnectCall() needs a <ConnectCallProvider> above it.");
55
+ }
56
+ return value;
57
+ }
58
+ /** How long the SDK may take to sign in before the attempt is abandoned. */
59
+ const LOGIN_TIMEOUT_MS = 15_000;
60
+ /** How long the "Call ended" card stays before the bar goes away. */
61
+ const ENDED_CARD_MS = 4_000;
62
+ /** How long after a call to re-read the balance - the carrier's report settles it. */
63
+ const AVAILABILITY_SETTLE_MS = 3_000;
64
+ function messageFromCause(cause) {
65
+ if (typeof cause === "string" && cause.trim())
66
+ return cause.trim();
67
+ return "The call couldn't be connected.";
68
+ }
69
+ function errorMessage(cause) {
70
+ return cause instanceof Error && cause.message ? cause.message : "Something went wrong.";
71
+ }
72
+ /**
73
+ * Holds the call in progress and draws the in-call bar. See the module comment.
74
+ */
75
+ function ConnectCallProvider({ children, onCallEnded, onError, className, }) {
76
+ const { mutateResource } = (0, provider_js_1.useConnect)();
77
+ const [call, setCall] = (0, react_1.useState)(null);
78
+ const { data: availability, refetch: refreshAvailability } = (0, provider_js_1.useConnectResource)("/calls/availability");
79
+ // Read inside `finish`, which is wired to SDK events once.
80
+ const refreshRef = (0, react_1.useRef)(refreshAvailability);
81
+ (0, react_1.useEffect)(() => {
82
+ refreshRef.current = refreshAvailability;
83
+ }, [refreshAvailability]);
84
+ const clientRef = (0, react_1.useRef)(null);
85
+ const placedRef = (0, react_1.useRef)(false);
86
+ const targetRef = (0, react_1.useRef)(null);
87
+ const callIdRef = (0, react_1.useRef)(null);
88
+ const endedRef = (0, react_1.useRef)(false);
89
+ const dismissTimer = (0, react_1.useRef)(null);
90
+ // Read inside SDK event handlers wired once, so they always see the latest.
91
+ const onCallEndedRef = (0, react_1.useRef)(onCallEnded);
92
+ (0, react_1.useEffect)(() => {
93
+ onCallEndedRef.current = onCallEnded;
94
+ }, [onCallEnded]);
95
+ const cancelOnServer = (0, react_1.useCallback)(() => {
96
+ const callId = callIdRef.current;
97
+ if (!callId)
98
+ return;
99
+ // Best effort: Connect closes an unclaimed call on its own within minutes.
100
+ void mutateResource("/calls/cancel", { callId }).catch(() => undefined);
101
+ }, [mutateResource]);
102
+ const finish = (0, react_1.useCallback)((notice) => {
103
+ if (endedRef.current)
104
+ return;
105
+ endedRef.current = true;
106
+ const callId = callIdRef.current;
107
+ const target = targetRef.current;
108
+ placedRef.current = false;
109
+ setCall((current) => current
110
+ ? {
111
+ ...current,
112
+ phase: "ended",
113
+ notice,
114
+ endedAfterSeconds: current.connectedAt
115
+ ? Math.round((Date.now() - current.connectedAt) / 1000)
116
+ : null,
117
+ }
118
+ : current);
119
+ if (dismissTimer.current)
120
+ clearTimeout(dismissTimer.current);
121
+ dismissTimer.current = setTimeout(() => setCall(null), ENDED_CARD_MS);
122
+ // The charge lands a moment after the line drops, so the minutes shown
123
+ // are re-read once it has had time to.
124
+ setTimeout(() => refreshRef.current(), AVAILABILITY_SETTLE_MS);
125
+ if (target) {
126
+ target.onEnded?.({ callId });
127
+ onCallEndedRef.current?.({ callId, phone: target.phone });
128
+ }
129
+ }, []);
130
+ const wire = (0, react_1.useCallback)((client) => {
131
+ const ringing = () => setCall((current) => (current ? { ...current, phase: "ringing" } : current));
132
+ client.on("onCalling", ringing);
133
+ client.on("onCallRemoteRinging", ringing);
134
+ client.on("onCallAnswered", () => setCall((current) => current ? { ...current, phase: "in-call", connectedAt: Date.now() } : current));
135
+ client.on("onCallTerminated", () => finish(null));
136
+ client.on("onCallFailed", (cause) => {
137
+ if (!placedRef.current)
138
+ cancelOnServer();
139
+ finish(messageFromCause(cause));
140
+ });
141
+ client.on("onMediaPermission", (event) => {
142
+ if (event?.status === "failure") {
143
+ cancelOnServer();
144
+ finish("Allow microphone access in your browser to make calls.");
145
+ }
146
+ });
147
+ }, [cancelOnServer, finish]);
148
+ const signIn = (0, react_1.useCallback)(async () => {
149
+ let client = clientRef.current;
150
+ if (!client) {
151
+ const { default: Plivo } = await Promise.resolve().then(() => __importStar(require("plivo-browser-sdk")));
152
+ client = new Plivo({
153
+ debug: "ERROR",
154
+ // Ask for the microphone when a call starts, not on page load.
155
+ permOnClick: true,
156
+ // Warn before a tab mid-call is closed.
157
+ closeProtection: true,
158
+ enableTracking: false,
159
+ }).client;
160
+ clientRef.current = client;
161
+ wire(client);
162
+ }
163
+ if (client.isLoggedIn)
164
+ return client;
165
+ const { token } = await mutateResource("/calls/browser-token", {});
166
+ const signedIn = client;
167
+ await new Promise((resolve, reject) => {
168
+ const timer = setTimeout(() => {
169
+ cleanup();
170
+ reject(new Error("Signing in to the phone service timed out."));
171
+ }, LOGIN_TIMEOUT_MS);
172
+ const onLogin = () => {
173
+ cleanup();
174
+ resolve();
175
+ };
176
+ const onLoginFailed = (cause) => {
177
+ cleanup();
178
+ reject(new Error(`Couldn't sign in to the phone service: ${messageFromCause(cause)}`));
179
+ };
180
+ function cleanup() {
181
+ clearTimeout(timer);
182
+ signedIn.off("onLogin", onLogin);
183
+ signedIn.off("onLoginFailed", onLoginFailed);
184
+ }
185
+ signedIn.on("onLogin", onLogin);
186
+ signedIn.on("onLoginFailed", onLoginFailed);
187
+ if (!signedIn.loginWithAccessToken(token)) {
188
+ cleanup();
189
+ reject(new Error("Couldn't sign in to the phone service."));
190
+ }
191
+ });
192
+ return signedIn;
193
+ }, [mutateResource, wire]);
194
+ const isBusy = call !== null && call.phase !== "ended";
195
+ const startCall = (0, react_1.useCallback)(async (target) => {
196
+ if (isBusy) {
197
+ onError?.("You're already on a call.");
198
+ return;
199
+ }
200
+ if (dismissTimer.current)
201
+ clearTimeout(dismissTimer.current);
202
+ targetRef.current = target;
203
+ callIdRef.current = null;
204
+ placedRef.current = false;
205
+ endedRef.current = false;
206
+ setCall({
207
+ phase: "connecting",
208
+ label: target.name?.trim() || target.phone,
209
+ number: target.phone,
210
+ callId: null,
211
+ connectedAt: null,
212
+ endedAfterSeconds: null,
213
+ muted: false,
214
+ notice: null,
215
+ });
216
+ try {
217
+ const client = await signIn();
218
+ const started = await mutateResource("/calls/start", {
219
+ phone: target.phone,
220
+ });
221
+ callIdRef.current = started.callId;
222
+ setCall((current) => current ? { ...current, callId: started.callId, number: started.toNumber } : current);
223
+ // Connect dials the number on the record, not this one - it is passed
224
+ // because the SDK requires a destination.
225
+ if (!client.call(started.toNumber.replace(/^\+/, ""), started.headers)) {
226
+ throw new Error("The phone service didn't accept the call.");
227
+ }
228
+ placedRef.current = true;
229
+ }
230
+ catch (cause) {
231
+ cancelOnServer();
232
+ // A refusal is usually news about the workspace - an empty balance, a
233
+ // number that has gone - so the buttons should learn it too.
234
+ refreshRef.current();
235
+ const message = errorMessage(cause);
236
+ if (onError) {
237
+ onError(message);
238
+ endedRef.current = true;
239
+ setCall(null);
240
+ target.onEnded?.({ callId: callIdRef.current });
241
+ onCallEndedRef.current?.({ callId: callIdRef.current, phone: target.phone });
242
+ }
243
+ else {
244
+ finish(message);
245
+ }
246
+ }
247
+ }, [cancelOnServer, finish, isBusy, mutateResource, onError, signIn]);
248
+ const hangUp = (0, react_1.useCallback)(() => {
249
+ const client = clientRef.current;
250
+ if (placedRef.current && client) {
251
+ client.hangup();
252
+ return; // `onCallTerminated` takes it from here.
253
+ }
254
+ cancelOnServer();
255
+ finish(null);
256
+ }, [cancelOnServer, finish]);
257
+ const toggleMute = (0, react_1.useCallback)(() => {
258
+ const client = clientRef.current;
259
+ if (!client)
260
+ return;
261
+ setCall((current) => {
262
+ if (!current)
263
+ return current;
264
+ if (current.muted)
265
+ client.unmute();
266
+ else
267
+ client.mute();
268
+ return { ...current, muted: !current.muted };
269
+ });
270
+ }, []);
271
+ // Sign out of the carrier when the provider unmounts.
272
+ (0, react_1.useEffect)(() => () => {
273
+ clientRef.current?.logout();
274
+ if (dismissTimer.current)
275
+ clearTimeout(dismissTimer.current);
276
+ }, []);
277
+ const value = (0, react_1.useMemo)(() => ({ isBusy, startCall, availability, refreshAvailability }), [isBusy, startCall, availability, refreshAvailability]);
278
+ return ((0, jsx_runtime_1.jsxs)(ConnectCallContext.Provider, { value: value, children: [children, call && ((0, jsx_runtime_1.jsx)(ConnectActiveCallBar, { call: call, onHangUp: hangUp, onToggleMute: toggleMute, className: className }))] }));
279
+ }
280
+ /**
281
+ * Rings `phone` from the workspace's number. Needs `<ConnectCallProvider>`
282
+ * above it, and a member with "Make calls" - render it only for them, since a
283
+ * button the server refuses for everyone else helps nobody.
284
+ */
285
+ function ConnectCallButton({ phone, name, onEnded, variant = "default", children, isDisabled, className, }) {
286
+ const { isBusy, startCall, availability } = useConnectCall();
287
+ const press = () => void startCall({ phone, name, onEnded });
288
+ // Not offered at all to someone who may not call - a button that always
289
+ // refuses helps nobody.
290
+ if (availability?.status === "no_permission")
291
+ return null;
292
+ // Anything else short of `available` says why, where the button would be,
293
+ // with the next step for whoever can take it.
294
+ if (availability && availability.status !== "available") {
295
+ return variant === "compact" ? ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "ghost", "aria-label": unavailableReason(availability).text, isDisabled: true, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Phone, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(ConnectCallUnavailable, { availability: availability, className: className }));
296
+ }
297
+ if (variant === "compact") {
298
+ return ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "ghost", "aria-label": `Call ${name?.trim() || phone}`, isDisabled: isBusy || isDisabled, onPress: press, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Phone, { className: "size-3.5" }) }));
299
+ }
300
+ return ((0, jsx_runtime_1.jsxs)(react_2.Button, { size: "sm", variant: "primary", isDisabled: isBusy || isDisabled || availability === null, onPress: press, className: (0, cn_js_1.cn)("shrink-0", className), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Phone, { className: "size-4" }), children ?? "Call"] }));
301
+ }
302
+ /**
303
+ * What to say when a member cannot call, and the one link that fixes it -
304
+ * offered only to someone with the permission to follow it.
305
+ */
306
+ function unavailableReason(availability) {
307
+ switch (availability.status) {
308
+ case "no_number":
309
+ return availability.canManageNumber && availability.offer
310
+ ? {
311
+ text: "Calling needs a business number.",
312
+ action: {
313
+ label: `Add ${availability.offer.addonName}`,
314
+ path: `/billing/subscriptions/${availability.offer.subscriptionId}`,
315
+ },
316
+ }
317
+ : { text: "Calling needs a business number - ask a workspace admin.", action: null };
318
+ case "number_pending":
319
+ return availability.canManageNumber
320
+ ? {
321
+ text: "Your business number is still being set up.",
322
+ action: { label: "Check calling", path: "/calling" },
323
+ }
324
+ : { text: "Your business number is still being set up.", action: null };
325
+ case "no_balance":
326
+ return availability.canTopUp
327
+ ? { text: "Out of calling minutes.", action: { label: "Top up", path: "/calling" } }
328
+ : { text: "Out of calling minutes - ask a workspace admin to top up.", action: null };
329
+ default:
330
+ return { text: "Calling isn't available.", action: null };
331
+ }
332
+ }
333
+ /** The inline "why not" a Call button becomes when the member cannot call. */
334
+ function ConnectCallUnavailable({ availability, className, }) {
335
+ const { text, action } = unavailableReason(availability);
336
+ const href = (0, portal_links_js_1.useConnectPortalUrl)(action?.path ?? "/calling");
337
+ return ((0, jsx_runtime_1.jsxs)("p", { className: (0, cn_js_1.cn)("text-xs text-muted", className), children: [text, " ", action && ((0, jsx_runtime_1.jsx)("a", { href: href, className: "font-medium text-accent hover:underline", children: action.label }))] }));
338
+ }
339
+ function formatRupees(paise) {
340
+ return `₹${(paise / 100).toLocaleString("en-IN", {
341
+ minimumFractionDigits: 2,
342
+ maximumFractionDigits: 2,
343
+ })}`;
344
+ }
345
+ /**
346
+ * The workspace's calling at a glance - its business number, the minutes
347
+ * left, and the next step - for a settings page. Reads the same availability
348
+ * the Call buttons do, so the two never disagree. Buying minutes and adding a
349
+ * number happen in Connect, and come back here when done.
350
+ */
351
+ function ConnectCallingStatus({ className }) {
352
+ const { availability } = useConnectCall();
353
+ const callingHref = (0, portal_links_js_1.useConnectPortalUrl)("/calling");
354
+ const offerHref = (0, portal_links_js_1.useConnectPortalUrl)(availability?.offer ? `/billing/subscriptions/${availability.offer.subscriptionId}` : "/calling");
355
+ if (!availability) {
356
+ return (0, jsx_runtime_1.jsx)("div", { className: (0, cn_js_1.cn)("h-24 animate-pulse rounded-lg bg-surface-secondary", className) });
357
+ }
358
+ const rate = formatRupees(availability.ratePaisePerMinute);
359
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("space-y-3 rounded-lg border border-separator bg-surface p-4", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "Business number" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium tabular-nums", children: availability.number ?? (availability.status === "number_pending" ? "Being set up…" : "None") })] }), availability.number && ((0, jsx_runtime_1.jsxs)("div", { className: "text-right", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "Calling minutes" }), (0, jsx_runtime_1.jsxs)("p", { className: (0, cn_js_1.cn)("text-sm font-medium tabular-nums", availability.lowBalance && "text-danger"), children: [formatRupees(availability.available), " \u00B7 about ", availability.availableMinutes, " min"] })] }))] }), availability.status === "no_permission" ? ((0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "You don't have permission to make calls here." })) : !availability.number ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Call customers from their record, from your own business number. Calls are not recorded, and minutes are prepaid at ", rate, " + GST a minute.", " ", availability.status === "number_pending"
360
+ ? availability.canManageNumber && ((0, jsx_runtime_1.jsx)("a", { href: callingHref, className: "font-medium text-accent hover:underline", children: "Check on your number" }))
361
+ : availability.canManageNumber && availability.offer
362
+ ? ((0, jsx_runtime_1.jsxs)("a", { href: offerHref, className: "font-medium text-accent hover:underline", children: ["Add ", availability.offer.addonName, " (", formatRupees(availability.offer.price), " + GST)"] }))
363
+ : "Ask a workspace admin to add a business number."] })) : ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [availability.lowBalance
364
+ ? "Calling minutes are running low. "
365
+ : `Calls cost ${rate} + GST a minute, from prepaid minutes. `, availability.canTopUp ? ((0, jsx_runtime_1.jsx)("a", { href: callingHref, className: "font-medium text-accent hover:underline", children: "Top up minutes" })) : ("A workspace admin can top up.")] }))] }));
366
+ }
367
+ const PHASE_LABEL = {
368
+ connecting: "Connecting…",
369
+ ringing: "Calling…",
370
+ ended: "Call ended",
371
+ };
372
+ function formatSeconds(seconds) {
373
+ const whole = Math.max(0, Math.round(seconds));
374
+ const minutes = Math.floor(whole / 60);
375
+ return `${minutes}:${String(whole % 60).padStart(2, "0")}`;
376
+ }
377
+ /** The floating in-call card. Exported for Storybook only - the provider draws it. */
378
+ function ConnectActiveCallBar({ call, onHangUp, onToggleMute, className, }) {
379
+ const elapsed = useElapsedSeconds(call.phase === "in-call" ? call.connectedAt : null);
380
+ const live = call.phase !== "ended";
381
+ const status = call.phase === "in-call"
382
+ ? formatSeconds(elapsed)
383
+ : call.phase === "ended" && call.endedAfterSeconds !== null
384
+ ? `Call ended · ${formatSeconds(call.endedAfterSeconds)}`
385
+ : PHASE_LABEL[call.phase];
386
+ return ((0, jsx_runtime_1.jsx)("div", { role: "status", "aria-live": "polite", className: (0, cn_js_1.cn)("fixed inset-x-0 bottom-4 z-50 flex justify-center px-4", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full max-w-sm items-center gap-3 rounded-2xl border border-separator bg-surface p-3 shadow-lg", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent", children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOutgoing, { "aria-hidden": true, className: "size-4" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-medium", children: call.label }), (0, jsx_runtime_1.jsxs)("p", { className: "truncate text-xs text-muted tabular-nums", children: [status, call.notice ? ` · ${call.notice}` : call.label === call.number ? "" : ` · ${call.number}`] })] }), live && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "secondary", "aria-label": call.muted ? "Unmute" : "Mute", isDisabled: call.phase !== "in-call", onPress: onToggleMute, children: call.muted ? (0, jsx_runtime_1.jsx)(lucide_react_1.MicOff, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Mic, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "danger", "aria-label": "Hang up", onPress: onHangUp, children: (0, jsx_runtime_1.jsx)(lucide_react_1.PhoneOff, { className: "size-4" }) })] }))] }) }));
387
+ }
388
+ /** Seconds since `since`, ticking once a second. `0` while there is nothing to time. */
389
+ function useElapsedSeconds(since) {
390
+ const [now, setNow] = (0, react_1.useState)(() => Date.now());
391
+ (0, react_1.useEffect)(() => {
392
+ if (since === null)
393
+ return;
394
+ const timer = setInterval(() => setNow(Date.now()), 1_000);
395
+ return () => clearInterval(timer);
396
+ }, [since]);
397
+ return since === null ? 0 : Math.max(0, Math.round((now - since) / 1000));
398
+ }
@@ -165,6 +165,7 @@ function ConnectCheckoutStatusRow({ checkout, }) {
165
165
  return ((0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "wait", initial: false, children: checkout.status !== "idle" && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -4 }, transition: { duration: 0.18 }, children: (0, jsx_runtime_1.jsx)(CheckoutStatusRowContent, { checkout: checkout }) }, checkout.status)) }));
166
166
  }
167
167
  function CheckoutStatusRowContent({ checkout, }) {
168
+ const { openConnect } = (0, provider_js_1.useConnect)();
168
169
  if (checkout.status === "waiting") {
169
170
  return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 rounded-lg border border-separator px-3 py-2 text-sm text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: "Finish up in the Connect window. This page updates on its own." })] }));
170
171
  }
@@ -175,7 +176,11 @@ function CheckoutStatusRowContent({ checkout, }) {
175
176
  // are fixed by going to the same URL - so offer it rather than explaining.
176
177
  return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3 rounded-lg border border-separator px-3 py-2 text-sm text-muted", children: [(0, jsx_runtime_1.jsx)("span", { children: checkout.status === "blocked"
177
178
  ? "Your browser blocked the Connect window."
178
- : "Nothing changed - the Connect window closed before it was finished." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [checkout.url && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(checkout.url, "_blank", "noopener,noreferrer"), children: ["Open Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })), (0, jsx_runtime_1.jsx)(react_1.Button, { variant: "tertiary", onPress: checkout.reset, children: "Dismiss" })] })] }));
179
+ : "Nothing changed - the Connect window closed before it was finished." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [checkout.url && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary",
180
+ // Not watched by the checkout poll - that run has ended - but a
181
+ // hand-off all the same: Connect reports back when it is done, and
182
+ // the provider re-reads every resource on this page.
183
+ onPress: () => openConnect(checkout.url), children: ["Open Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })), (0, jsx_runtime_1.jsx)(react_1.Button, { variant: "tertiary", onPress: checkout.reset, children: "Dismiss" })] })] }));
179
184
  }
180
185
  /**
181
186
  * Cancellation, kept out of the plan grid and stated plainly.
@@ -195,9 +200,10 @@ function CheckoutStatusRowContent({ checkout, }) {
195
200
  * version.
196
201
  */
197
202
  function ConnectUnsubscribeRow({ subscription, appUrl, checkout, }) {
203
+ const { openConnect } = (0, provider_js_1.useConnect)();
198
204
  const endsOn = subscription.currentBillingCycle?.endDate;
199
205
  if (subscription.cancelAtPeriodEnd) {
200
- return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t border-separator pt-4", children: [(0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: ["Cancels", endsOn ? ` on ${new Date(endsOn).toLocaleDateString()}` : " at the end of this cycle", ". You keep access until then."] }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${subscription.id}`, "_blank", "noopener,noreferrer"), children: ["View on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] }));
206
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t border-separator pt-4", children: [(0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: ["Cancels", endsOn ? ` on ${new Date(endsOn).toLocaleDateString()}` : " at the end of this cycle", ". You keep access until then."] }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", onPress: () => openConnect(`/billing/subscriptions/${encodeURIComponent(subscription.id)}`), children: ["View on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] }));
201
207
  }
202
208
  return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3 border-t border-separator pt-4", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Cancel any time. Access continues to the end of the paid cycle." }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: "text-danger", isDisabled: checkout.status === "waiting", onPress: () => checkout.start({
203
209
  url: (0, use_checkout_js_1.buildCancelUrl)({ appUrl, subscriptionId: subscription.id }),
@@ -72,9 +72,10 @@ function ConnectReadinessBanner({ includeAdvisory = false, hideWhenReady = true,
72
72
  return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: (0, cn_js_1.cn)("flex flex-col gap-2", className), role: "status", initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: motion_js_1.ENTER, children: blockers.map((blocker) => ((0, jsx_runtime_1.jsx)(BlockerRow, { blocker: blocker }, `${blocker.code}-${blocker.message}`))) }) }));
73
73
  }
74
74
  function BlockerRow({ blocker }) {
75
+ const { openConnect } = (0, provider_js_1.useConnect)();
75
76
  const inProgress = IN_PROGRESS_CODES.has(blocker.code);
76
77
  const isBlocking = blocker.severity === "blocking";
77
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-wrap items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm ${inProgress || !isBlocking ? "bg-surface text-muted" : "bg-danger-soft text-danger"}`, children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [inProgress || !isBlocking ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "size-4 shrink-0", "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.AlertTriangle, { className: "size-4 shrink-0", "aria-hidden": "true" })), blocker.message] }), blocker.resolveUrl && !inProgress && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(blocker.resolveUrl, "_blank", "noopener,noreferrer"), children: [actionLabel(blocker), (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }))] }));
78
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-wrap items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm ${inProgress || !isBlocking ? "bg-surface text-muted" : "bg-danger-soft text-danger"}`, children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [inProgress || !isBlocking ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "size-4 shrink-0", "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.AlertTriangle, { className: "size-4 shrink-0", "aria-hidden": "true" })), blocker.message] }), blocker.resolveUrl && !inProgress && ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => openConnect(blocker.resolveUrl), children: [actionLabel(blocker), (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }))] }));
78
79
  }
79
80
  function ReadyRow({ className }) {
80
81
  return ((0, jsx_runtime_1.jsx)("p", { className: (0, cn_js_1.cn)("text-sm text-muted", className), children: "This workspace is cleared to use every part of the application." }));
@@ -80,13 +80,13 @@ function ConnectNoSubscriptionCard({ plansHref = DEFAULT_PLANS_HREF, title = "Yo
80
80
  * impure and re-renders wrong.
81
81
  */
82
82
  function ConnectSubscriptionCard({ plansHref, className }) {
83
- const { appUrl } = (0, provider_js_1.useConnect)();
83
+ const { openConnect } = (0, provider_js_1.useConnect)();
84
84
  const { isLoading, detail, plan, state } = useSubscriptionState();
85
85
  if (isLoading)
86
86
  return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: (0, cn_js_1.cn)("h-48 rounded-xl", className) });
87
87
  if (!detail || !plan)
88
88
  return (0, jsx_runtime_1.jsx)(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
89
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
89
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => openConnect(`/billing/subscriptions/${encodeURIComponent(detail.id)}`), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
90
90
  }
91
91
  function SubscriptionChip({ state }) {
92
92
  switch (state) {
@@ -157,11 +157,11 @@ function ConnectSeatUsage({ plan, detail, className, }) {
157
157
  * actually cares about anyway.
158
158
  */
159
159
  function ConnectTrialBanner({ plansHref, className }) {
160
- const { appUrl } = (0, provider_js_1.useConnect)();
160
+ const { openConnect } = (0, provider_js_1.useConnect)();
161
161
  const { detail, state } = useSubscriptionState();
162
162
  if (state !== "trialing" || !detail)
163
163
  return null;
164
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] })] }));
164
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => openConnect(`/billing/subscriptions/${encodeURIComponent(detail.id)}`), children: ["Manage", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] })] }));
165
165
  }
166
166
  /**
167
167
  * Renders `children` for a subscribed workspace, and the "pick a plan" card for
@@ -13,7 +13,6 @@ const jsx_runtime_1 = require("react/jsx-runtime");
13
13
  const react_1 = require("@heroui/react");
14
14
  const lucide_react_1 = require("lucide-react");
15
15
  const provider_js_1 = require("../provider.js");
16
- const portal_links_js_1 = require("../portal-links.js");
17
16
  const mark_js_1 = require("../mark.js");
18
17
  const identity_js_1 = require("./identity.js");
19
18
  const account_sessions_js_1 = require("./account-sessions.js");
@@ -26,6 +25,12 @@ const THEME_OPTIONS = [
26
25
  { value: "dark", label: "Dark", icon: lucide_react_1.Moon },
27
26
  { value: "system", label: "System", icon: lucide_react_1.Laptop },
28
27
  ];
28
+ /**
29
+ * Connect's front page, which is a destination rather than a task - nothing to
30
+ * finish, so nothing to come back from. Every *task* destination here (Profile,
31
+ * Billing, Invoices) goes through `openConnect` instead, which brings the
32
+ * visitor back when they are done.
33
+ */
29
34
  function openExternal(url) {
30
35
  window.open(url, "_blank", "noopener,noreferrer");
31
36
  }
@@ -100,8 +105,8 @@ function subscriptionChip(subscription) {
100
105
  * 44%, and takes the tile down to ~88px while making its contents read
101
106
  * bigger, not smaller.
102
107
  */
103
- function DestinationTile({ icon, label, url, }) {
104
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className: `relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-3 hover:border-accent hover:bg-surface-secondary`, onPress: () => openExternal(url), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "absolute end-2 top-2 size-3.5 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: `flex ${surfaces_js_1.MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-foreground", children: label })] }));
108
+ function DestinationTile({ icon, label, onPress, newTab, }) {
109
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": newTab ? `${label} (opens in a new tab)` : label, className: `relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-3 hover:border-accent hover:bg-surface-secondary`, onPress: onPress, children: [newTab && (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "absolute end-2 top-2 size-3.5 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: `flex ${surfaces_js_1.MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-foreground", children: label })] }));
105
110
  }
106
111
  /**
107
112
  * The account modal's contents - identity card, the three Connect
@@ -122,14 +127,14 @@ function DestinationTile({ icon, label, url, }) {
122
127
  * three navigations.
123
128
  */
124
129
  function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
125
- const { session, signOut, appUrl, clientId } = (0, provider_js_1.useConnect)();
130
+ const { session, signOut, appUrl, openConnect, handoffMode } = (0, provider_js_1.useConnect)();
126
131
  const user = session?.user;
127
132
  if (!user)
128
133
  return null;
129
134
  const label = user.displayName ?? user.email ?? "Account";
130
135
  const role = session?.role ?? null;
131
136
  const planChip = subscriptionChip(session?.subscription ?? null);
132
- return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: (0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile") }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
137
+ return ((0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", onPress: () => openConnect("/profile"), newTab: handoffMode === "popup" }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", onPress: () => openConnect("/billing"), newTab: handoffMode === "popup" }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", onPress: () => openExternal(appUrl), newTab: true })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
133
138
  const [next] = [...keys];
134
139
  if (typeof next === "string")
135
140
  onThemeChange(next);
@@ -207,8 +212,8 @@ function ConnectUserMenu({ compact = false, ...rest }) {
207
212
  * state, a settings page.
208
213
  */
209
214
  function ConnectManageSubscriptionButton({ className }) {
210
- const { appUrl } = (0, provider_js_1.useConnect)();
211
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () => openExternal(`${appUrl}/billing`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: "size-4 shrink-0" }), "Manage subscription", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
215
+ const { openConnect } = (0, provider_js_1.useConnect)();
216
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () => openConnect("/billing"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: "size-4 shrink-0" }), "Manage subscription", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
212
217
  }
213
218
  /**
214
219
  * Invoices and payment history, on Connect.
@@ -221,10 +226,10 @@ function ConnectManageSubscriptionButton({ className }) {
221
226
  * version of a record that must have exactly one.
222
227
  */
223
228
  function ConnectInvoicesButton({ className }) {
224
- const { session, appUrl } = (0, provider_js_1.useConnect)();
229
+ const { session, openConnect } = (0, provider_js_1.useConnect)();
225
230
  if (!session?.user)
226
231
  return null;
227
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(`${appUrl}/billing/invoices`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ReceiptText, { className: "size-4 shrink-0" }), "Invoices", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
232
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openConnect("/billing/invoices"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ReceiptText, { className: "size-4 shrink-0" }), "Invoices", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
228
233
  }
229
234
  /**
230
235
  * The "Profile" action on its own - the third of the three Connect
@@ -236,10 +241,10 @@ function ConnectInvoicesButton({ className }) {
236
241
  * and got no `return_to`, so the user had no way back.
237
242
  */
238
243
  function ConnectProfileButton({ className }) {
239
- const { session, appUrl, clientId } = (0, provider_js_1.useConnect)();
244
+ const { session, openConnect } = (0, provider_js_1.useConnect)();
240
245
  if (!session?.user)
241
246
  return null;
242
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal((0, portal_links_js_1.connectPortalUrl)(appUrl, clientId, "/profile")), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
247
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openConnect("/profile"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
243
248
  }
244
249
  /** A plain "Open Connect" button, for a footer or a help menu. */
245
250
  function ConnectHomeButton({ className }) {