@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.
- package/CHANGELOG.md +124 -0
- package/dist/cjs/client/components/access-gate.js +18 -40
- package/dist/cjs/client/components/apps-grid.js +11 -3
- package/dist/cjs/client/components/ask-ai-errors.js +109 -0
- package/dist/cjs/client/components/ask-ai-turns.js +12 -0
- package/dist/cjs/client/components/ask-ai.js +75 -8
- package/dist/cjs/client/components/call-button.js +398 -0
- package/dist/cjs/client/components/plans.js +8 -2
- package/dist/cjs/client/components/readiness.js +2 -1
- package/dist/cjs/client/components/subscription.js +4 -4
- package/dist/cjs/client/components/user-menu.js +16 -11
- package/dist/cjs/client/handoff.js +78 -0
- package/dist/cjs/client/index.js +15 -7
- package/dist/cjs/client/portal-links.js +1 -1
- package/dist/cjs/client/provider.js +75 -4
- package/dist/cjs/client/use-checkout.js +23 -16
- package/dist/cjs/server/client.js +5 -1
- package/dist/cjs/server/handler.js +77 -1
- package/dist/cjs/server/resources/calls.js +58 -0
- package/dist/cjs/server/resources/identity.js +12 -0
- package/dist/cjs/server/resources/whatsapp.js +58 -0
- package/dist/esm/client/components/access-gate.js +18 -40
- package/dist/esm/client/components/apps-grid.js +11 -3
- package/dist/esm/client/components/ask-ai-errors.js +102 -0
- package/dist/esm/client/components/ask-ai-turns.js +12 -0
- package/dist/esm/client/components/ask-ai.js +76 -9
- package/dist/esm/client/components/call-button.js +358 -0
- package/dist/esm/client/components/plans.js +8 -2
- package/dist/esm/client/components/readiness.js +2 -1
- package/dist/esm/client/components/subscription.js +4 -4
- package/dist/esm/client/components/user-menu.js +16 -11
- package/dist/esm/client/handoff.js +72 -0
- package/dist/esm/client/index.js +6 -4
- package/dist/esm/client/portal-links.js +1 -1
- package/dist/esm/client/provider.js +75 -4
- package/dist/esm/client/use-checkout.js +23 -16
- package/dist/esm/server/client.js +5 -1
- package/dist/esm/server/handler.js +77 -1
- package/dist/esm/server/resources/calls.js +58 -0
- package/dist/esm/server/resources/identity.js +12 -0
- package/dist/esm/server/resources/whatsapp.js +54 -0
- package/dist/types/client/components/ask-ai-errors.d.ts +41 -0
- package/dist/types/client/components/ask-ai-turns.d.ts +5 -0
- package/dist/types/client/components/call-button.d.ts +132 -0
- package/dist/types/client/handoff.d.ts +62 -0
- package/dist/types/client/index.d.ts +5 -1
- package/dist/types/client/portal-links.d.ts +1 -1
- package/dist/types/client/provider.d.ts +72 -5
- package/dist/types/client/use-checkout.d.ts +4 -1
- package/dist/types/server/client.d.ts +4 -1
- package/dist/types/server/resources/calls.d.ts +32 -1
- package/dist/types/server/resources/identity.d.ts +10 -0
- package/dist/types/server/resources/whatsapp.d.ts +44 -0
- package/dist/types/types.d.ts +152 -1
- 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",
|
|
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: () =>
|
|
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: () =>
|
|
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 {
|
|
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: () =>
|
|
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 {
|
|
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: () =>
|
|
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,
|
|
104
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)
|
|
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,
|
|
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",
|
|
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 {
|
|
211
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () =>
|
|
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,
|
|
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: () =>
|
|
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,
|
|
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: () =>
|
|
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 }) {
|