@agent-native/core 0.84.16 → 0.84.18
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/session-replay.ts +2 -2
- package/corpus/templates/analytics/changelog/2026-07-01-session-replay-recordings-upload-reliably-in-production.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +46 -5
- package/corpus/templates/calendar/app/components/booking/TimeSlotPicker.tsx +10 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +15 -6
- package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +22 -0
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +6 -2
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/calendar/app/i18n-data.ts +107 -0
- package/corpus/templates/calendar/app/lib/google-oauth-setup.ts +17 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +9 -1
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +40 -26
- package/corpus/templates/calendar/app/pages/Settings.tsx +3 -1
- package/corpus/templates/calendar/changelog/2026-07-01-booking-links-now-show-an-error-when-calendar-availability-c.md +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +78 -13
- package/corpus/templates/calendar/server/handlers/google-auth.ts +3 -2
- package/corpus/templates/calendar/server/lib/google-calendar.ts +16 -2
- package/corpus/templates/design/app/lib/design-system-preview.ts +69 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +71 -50
- package/corpus/templates/design/changelog/2026-07-01-design-systems-no-longer-crash-on-responsive-tokens.md +6 -0
- package/dist/client/session-replay.js +2 -2
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fb0021b: Send compressed session replay uploads as binary payloads so Netlify preserves gzip bytes.
|
|
8
|
+
|
|
9
|
+
## 0.84.17
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a4f5303: Keep background chat continuation markers and database pool detection aligned with the actual background function dispatch path.
|
|
14
|
+
|
|
3
15
|
## 0.84.16
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.18",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -761,7 +761,7 @@ async function gzipReplayBody(body: string): Promise<Blob | null> {
|
|
|
761
761
|
.stream()
|
|
762
762
|
.pipeThrough(new CompressionStream("gzip"));
|
|
763
763
|
const compressed = await new Response(stream).arrayBuffer();
|
|
764
|
-
return new Blob([compressed], { type: "application/
|
|
764
|
+
return new Blob([compressed], { type: "application/octet-stream" });
|
|
765
765
|
} catch {
|
|
766
766
|
return null;
|
|
767
767
|
}
|
|
@@ -774,7 +774,7 @@ async function buildReplayUploadBody(body: string): Promise<ReplayUploadBody> {
|
|
|
774
774
|
body: compressed,
|
|
775
775
|
compressed: true,
|
|
776
776
|
headers: {
|
|
777
|
-
"Content-Type": "application/
|
|
777
|
+
"Content-Type": "application/octet-stream",
|
|
778
778
|
"Content-Encoding": "gzip",
|
|
779
779
|
},
|
|
780
780
|
};
|
|
@@ -82,11 +82,36 @@ function statusError(message: string, statusCode: number): Error {
|
|
|
82
82
|
return Object.assign(new Error(message), { statusCode });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function looksLikeDecodedJson(bytes: Buffer): boolean {
|
|
86
|
+
const first = bytes.toString("utf8").trimStart()[0];
|
|
87
|
+
return first === "{" || first === "[";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tryGunzip(bytes: Buffer): Buffer | null {
|
|
91
|
+
try {
|
|
92
|
+
return gunzipSync(bytes);
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeTextWrappedGzip(
|
|
99
|
+
bytes: Buffer,
|
|
100
|
+
): { decoded: Buffer; requestBytes: number } | null {
|
|
101
|
+
const text = bytes.toString("utf8");
|
|
102
|
+
const binaryStringBytes = Buffer.from(text, "latin1");
|
|
103
|
+
if (binaryStringBytes.equals(bytes)) return null;
|
|
104
|
+
const decoded = tryGunzip(binaryStringBytes);
|
|
105
|
+
return decoded
|
|
106
|
+
? { decoded, requestBytes: binaryStringBytes.byteLength }
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
export function decodeSessionReplayRequestBody(
|
|
86
111
|
rawBody: Buffer | Uint8Array | string | undefined,
|
|
87
112
|
contentEncoding?: string | null,
|
|
88
113
|
): { body: unknown; requestBytes: number } {
|
|
89
|
-
|
|
114
|
+
let requestBytes =
|
|
90
115
|
typeof rawBody === "string"
|
|
91
116
|
? Buffer.byteLength(rawBody, "utf8")
|
|
92
117
|
: (rawBody?.byteLength ?? 0);
|
|
@@ -99,10 +124,26 @@ export function decodeSessionReplayRequestBody(
|
|
|
99
124
|
let decoded = bytes;
|
|
100
125
|
|
|
101
126
|
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
const gunzipped = tryGunzip(bytes);
|
|
128
|
+
if (gunzipped) {
|
|
129
|
+
decoded = gunzipped;
|
|
130
|
+
} else {
|
|
131
|
+
// Netlify may hand Nitro an already-decoded body while preserving the
|
|
132
|
+
// original browser Content-Encoding header.
|
|
133
|
+
if (looksLikeDecodedJson(bytes)) {
|
|
134
|
+
decoded = bytes;
|
|
135
|
+
} else {
|
|
136
|
+
// Some Netlify paths wrap binary request bodies in a JS string before
|
|
137
|
+
// Nitro reads them back as UTF-8. Reinterpret that text as one-byte
|
|
138
|
+
// binary data so real browser CompressionStream uploads survive.
|
|
139
|
+
const textWrappedGzip = decodeTextWrappedGzip(bytes);
|
|
140
|
+
if (textWrappedGzip) {
|
|
141
|
+
decoded = textWrappedGzip.decoded;
|
|
142
|
+
requestBytes = textWrappedGzip.requestBytes;
|
|
143
|
+
} else {
|
|
144
|
+
throw statusError("Invalid gzip-compressed replay body", 400);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
106
147
|
}
|
|
107
148
|
} else if (encoding && encoding !== "identity") {
|
|
108
149
|
throw statusError(
|
|
@@ -9,6 +9,7 @@ interface TimeSlotPickerProps {
|
|
|
9
9
|
selectedSlot: string | null;
|
|
10
10
|
onSelect: (start: string) => void;
|
|
11
11
|
loading?: boolean;
|
|
12
|
+
errorMessage?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function TimeSlotPicker({
|
|
@@ -16,6 +17,7 @@ export function TimeSlotPicker({
|
|
|
16
17
|
selectedSlot,
|
|
17
18
|
onSelect,
|
|
18
19
|
loading,
|
|
20
|
+
errorMessage,
|
|
19
21
|
}: TimeSlotPickerProps) {
|
|
20
22
|
const t = useT();
|
|
21
23
|
|
|
@@ -29,6 +31,14 @@ export function TimeSlotPicker({
|
|
|
29
31
|
);
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
if (errorMessage) {
|
|
35
|
+
return (
|
|
36
|
+
<p className="rounded-lg border border-destructive/30 bg-destructive/[0.06] px-3 py-3 text-sm text-destructive">
|
|
37
|
+
{errorMessage}
|
|
38
|
+
</p>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
if (slots.length === 0) {
|
|
33
43
|
return (
|
|
34
44
|
<p className="text-sm text-muted-foreground text-center py-4">
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
useDisconnectGoogle,
|
|
34
34
|
type DesktopAuthIssue,
|
|
35
35
|
} from "@/hooks/use-google-auth";
|
|
36
|
+
import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
|
|
36
37
|
|
|
37
38
|
interface EnvKeyStatus {
|
|
38
39
|
key: string;
|
|
@@ -93,6 +94,7 @@ export function GoogleConnectBanner({
|
|
|
93
94
|
|
|
94
95
|
const accounts = googleStatus.data?.accounts ?? [];
|
|
95
96
|
const hasAccounts = accounts.length > 0;
|
|
97
|
+
const canOfferOAuthSetup = useMemo(() => shouldOfferGoogleOAuthSetup(), []);
|
|
96
98
|
|
|
97
99
|
const isBuilderFrame = useMemo(() => isInBuilderFrame(), []);
|
|
98
100
|
const authPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -184,10 +186,17 @@ export function GoogleConnectBanner({
|
|
|
184
186
|
useEffect(() => {
|
|
185
187
|
if (authUrl.error) {
|
|
186
188
|
setWantAuthUrl(false);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
if (canOfferOAuthSetup) {
|
|
190
|
+
setShowWizard(true);
|
|
191
|
+
fetchStatus();
|
|
192
|
+
} else {
|
|
193
|
+
setDesktopAuthIssue({
|
|
194
|
+
code: "managed_credentials_unavailable",
|
|
195
|
+
message: t("googleConnect.managedCredentialsUnavailableDescription"),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
189
198
|
}
|
|
190
|
-
}, [authUrl.error, fetchStatus]);
|
|
199
|
+
}, [authUrl.error, canOfferOAuthSetup, fetchStatus, t]);
|
|
191
200
|
|
|
192
201
|
useEffect(() => {
|
|
193
202
|
if (
|
|
@@ -388,7 +397,7 @@ export function GoogleConnectBanner({
|
|
|
388
397
|
</div>
|
|
389
398
|
)}
|
|
390
399
|
|
|
391
|
-
{showWizard && !allConfigured && (
|
|
400
|
+
{showWizard && !allConfigured && canOfferOAuthSetup && (
|
|
392
401
|
<div className="mt-8 w-full max-w-lg text-start">
|
|
393
402
|
<SetupWizard
|
|
394
403
|
currentStep={currentStep}
|
|
@@ -481,7 +490,7 @@ export function GoogleConnectBanner({
|
|
|
481
490
|
</div>
|
|
482
491
|
|
|
483
492
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
484
|
-
{showWizard && !allConfigured ? (
|
|
493
|
+
{showWizard && !allConfigured && canOfferOAuthSetup ? (
|
|
485
494
|
<Button
|
|
486
495
|
size="sm"
|
|
487
496
|
variant="outline"
|
|
@@ -541,7 +550,7 @@ export function GoogleConnectBanner({
|
|
|
541
550
|
/>
|
|
542
551
|
|
|
543
552
|
{/* Inline setup wizard */}
|
|
544
|
-
{showWizard && !allConfigured && (
|
|
553
|
+
{showWizard && !allConfigured && canOfferOAuthSetup && (
|
|
545
554
|
<div className="px-5 pb-4 pt-1 max-w-2xl">
|
|
546
555
|
<p className="text-xs text-muted-foreground mb-3">
|
|
547
556
|
{t("googleConnect.followSteps")}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@agent-native/core/client";
|
|
6
6
|
import {
|
|
7
7
|
IconExternalLink,
|
|
8
|
+
IconAlertTriangle,
|
|
8
9
|
IconCheck,
|
|
9
10
|
IconCircle,
|
|
10
11
|
IconLoader2,
|
|
@@ -15,6 +16,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|
|
15
16
|
import { Button } from "@/components/ui/button";
|
|
16
17
|
import { Input } from "@/components/ui/input";
|
|
17
18
|
import { Label } from "@/components/ui/label";
|
|
19
|
+
import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
|
|
18
20
|
|
|
19
21
|
interface EnvKeyStatus {
|
|
20
22
|
key: string;
|
|
@@ -92,6 +94,26 @@ export function GoogleSetupWizard() {
|
|
|
92
94
|
const allConfigured =
|
|
93
95
|
envStatus.length > 0 && envStatus.every((k) => k.configured);
|
|
94
96
|
|
|
97
|
+
if (!shouldOfferGoogleOAuthSetup()) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="rounded-lg border border-amber-500/25 bg-amber-500/[0.07] p-4 text-start">
|
|
100
|
+
<div className="flex items-start gap-3">
|
|
101
|
+
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-amber-500/15 text-amber-300">
|
|
102
|
+
<IconAlertTriangle className="h-4 w-4" />
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<p className="text-sm font-medium text-foreground">
|
|
106
|
+
{t("googleConnect.managedCredentialsUnavailable")}
|
|
107
|
+
</p>
|
|
108
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
109
|
+
{t("googleConnect.managedCredentialsUnavailableDescription")}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
async function handleJsonUpload(file: File) {
|
|
96
118
|
setSaving(true);
|
|
97
119
|
setError(null);
|
|
@@ -30,7 +30,9 @@ export function useAvailableSlots(
|
|
|
30
30
|
const res = await fetch(
|
|
31
31
|
appApiPath(`/api/bookings/available-slots?${params}`),
|
|
32
32
|
);
|
|
33
|
-
if (!res.ok)
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(await readErrorMessage(res, "Failed to fetch slots"));
|
|
35
|
+
}
|
|
34
36
|
const data = await res.json();
|
|
35
37
|
return Array.isArray(data) ? data : (data.slots ?? []);
|
|
36
38
|
},
|
|
@@ -57,7 +59,9 @@ export function useAvailableDays(
|
|
|
57
59
|
const res = await fetch(
|
|
58
60
|
appApiPath(`/api/bookings/available-slots?${params}`),
|
|
59
61
|
);
|
|
60
|
-
if (!res.ok)
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(await readErrorMessage(res, "Failed to fetch days"));
|
|
64
|
+
}
|
|
61
65
|
const data = await res.json();
|
|
62
66
|
return Array.isArray(data?.dates) ? data.dates : [];
|
|
63
67
|
},
|
|
@@ -375,6 +375,9 @@ const messages = {
|
|
|
375
375
|
googleWarningAfterUnsafe: "完成連線。",
|
|
376
376
|
accountConnectedElsewhere: "該帳戶已連線到另一個登入帳戶",
|
|
377
377
|
googleConnectionFailed: "Google 連線失敗",
|
|
378
|
+
managedCredentialsUnavailable: "Google Calendar 暫時無法使用",
|
|
379
|
+
managedCredentialsUnavailableDescription:
|
|
380
|
+
"此部署尚未設定 Calendar 的 Google 連線。請稍後再試。",
|
|
378
381
|
thatGoogleAccount: "那個 Google 帳戶",
|
|
379
382
|
signOutThenSignIn: "登出,然後使用 {{account}} 登入。",
|
|
380
383
|
signOut: "登出",
|
|
@@ -427,6 +430,7 @@ const messages = {
|
|
|
427
430
|
availabilityDescription: "設定你的預約可用時間。",
|
|
428
431
|
availabilitySaved: "已儲存可用性",
|
|
429
432
|
availabilitySaveFailed: "無法儲存可用性",
|
|
433
|
+
availabilityUnavailable: "目前無法檢查此主辦人的行事曆可用性。請稍後再試。",
|
|
430
434
|
availableTimes: "可用時間",
|
|
431
435
|
allCount: "全部({{count}})",
|
|
432
436
|
back: "返回",
|
|
@@ -398,6 +398,9 @@ const enUS = {
|
|
|
398
398
|
googleWarningAfterUnsafe: "to finish connecting.",
|
|
399
399
|
accountConnectedElsewhere: "This account is connected to another login",
|
|
400
400
|
googleConnectionFailed: "Google connection failed",
|
|
401
|
+
managedCredentialsUnavailable: "Google Calendar is unavailable",
|
|
402
|
+
managedCredentialsUnavailableDescription:
|
|
403
|
+
"Calendar's Google connection is not configured for this deployment. Please try again later.",
|
|
401
404
|
thatGoogleAccount: "that Google account",
|
|
402
405
|
signOutThenSignIn: "Sign out, then sign in with {{account}}.",
|
|
403
406
|
signOut: "Sign out",
|
|
@@ -452,6 +455,8 @@ const enUS = {
|
|
|
452
455
|
availabilityDescription: "Set your available hours for bookings.",
|
|
453
456
|
availabilitySaved: "Availability saved",
|
|
454
457
|
availabilitySaveFailed: "Failed to save availability",
|
|
458
|
+
availabilityUnavailable:
|
|
459
|
+
"This host's calendar availability can't be checked right now. Please try again later.",
|
|
455
460
|
availableTimes: "Available Times",
|
|
456
461
|
allCount: "All ({{count}})",
|
|
457
462
|
back: "Back",
|
|
@@ -9233,6 +9238,107 @@ const translatedCalendarRawBurnDown = {
|
|
|
9233
9238
|
},
|
|
9234
9239
|
} satisfies Partial<Record<LocaleCode, PartialMessages>>;
|
|
9235
9240
|
|
|
9241
|
+
const translatedCalendarAvailabilityFix = {
|
|
9242
|
+
"zh-CN": {
|
|
9243
|
+
googleConnect: {
|
|
9244
|
+
managedCredentialsUnavailable: "Google Calendar 暂不可用",
|
|
9245
|
+
managedCredentialsUnavailableDescription:
|
|
9246
|
+
"此部署尚未配置 Calendar 的 Google 连接。请稍后再试。",
|
|
9247
|
+
},
|
|
9248
|
+
bookingLinks: {
|
|
9249
|
+
availabilityUnavailable: "目前无法检查此主办人的日历可用性。请稍后再试。",
|
|
9250
|
+
},
|
|
9251
|
+
},
|
|
9252
|
+
"es-ES": {
|
|
9253
|
+
googleConnect: {
|
|
9254
|
+
managedCredentialsUnavailable: "Google Calendar no está disponible",
|
|
9255
|
+
managedCredentialsUnavailableDescription:
|
|
9256
|
+
"La conexión de Google de Calendar no está configurada para este despliegue. Inténtalo de nuevo más tarde.",
|
|
9257
|
+
},
|
|
9258
|
+
bookingLinks: {
|
|
9259
|
+
availabilityUnavailable:
|
|
9260
|
+
"No se puede comprobar ahora la disponibilidad del calendario de este anfitrión. Inténtalo de nuevo más tarde.",
|
|
9261
|
+
},
|
|
9262
|
+
},
|
|
9263
|
+
"fr-FR": {
|
|
9264
|
+
googleConnect: {
|
|
9265
|
+
managedCredentialsUnavailable: "Google Calendar est indisponible",
|
|
9266
|
+
managedCredentialsUnavailableDescription:
|
|
9267
|
+
"La connexion Google de Calendar n'est pas configurée pour ce déploiement. Réessayez plus tard.",
|
|
9268
|
+
},
|
|
9269
|
+
bookingLinks: {
|
|
9270
|
+
availabilityUnavailable:
|
|
9271
|
+
"La disponibilité du calendrier de cet hôte ne peut pas être vérifiée pour le moment. Réessayez plus tard.",
|
|
9272
|
+
},
|
|
9273
|
+
},
|
|
9274
|
+
"de-DE": {
|
|
9275
|
+
googleConnect: {
|
|
9276
|
+
managedCredentialsUnavailable: "Google Calendar ist nicht verfügbar",
|
|
9277
|
+
managedCredentialsUnavailableDescription:
|
|
9278
|
+
"Die Google-Verbindung von Calendar ist für diese Bereitstellung nicht konfiguriert. Bitte versuche es später erneut.",
|
|
9279
|
+
},
|
|
9280
|
+
bookingLinks: {
|
|
9281
|
+
availabilityUnavailable:
|
|
9282
|
+
"Die Kalenderverfügbarkeit dieses Gastgebers kann derzeit nicht geprüft werden. Bitte versuche es später erneut.",
|
|
9283
|
+
},
|
|
9284
|
+
},
|
|
9285
|
+
"ja-JP": {
|
|
9286
|
+
googleConnect: {
|
|
9287
|
+
managedCredentialsUnavailable: "Google Calendar を利用できません",
|
|
9288
|
+
managedCredentialsUnavailableDescription:
|
|
9289
|
+
"このデプロイでは Calendar の Google 接続が設定されていません。後でもう一度お試しください。",
|
|
9290
|
+
},
|
|
9291
|
+
bookingLinks: {
|
|
9292
|
+
availabilityUnavailable:
|
|
9293
|
+
"この主催者のカレンダーの空き状況を現在確認できません。後でもう一度お試しください。",
|
|
9294
|
+
},
|
|
9295
|
+
},
|
|
9296
|
+
"ko-KR": {
|
|
9297
|
+
googleConnect: {
|
|
9298
|
+
managedCredentialsUnavailable: "Google Calendar를 사용할 수 없습니다",
|
|
9299
|
+
managedCredentialsUnavailableDescription:
|
|
9300
|
+
"이 배포에는 Calendar의 Google 연결이 구성되어 있지 않습니다. 나중에 다시 시도하세요.",
|
|
9301
|
+
},
|
|
9302
|
+
bookingLinks: {
|
|
9303
|
+
availabilityUnavailable:
|
|
9304
|
+
"현재 이 주최자의 캘린더 가능 시간을 확인할 수 없습니다. 나중에 다시 시도하세요.",
|
|
9305
|
+
},
|
|
9306
|
+
},
|
|
9307
|
+
"pt-BR": {
|
|
9308
|
+
googleConnect: {
|
|
9309
|
+
managedCredentialsUnavailable: "Google Calendar indisponível",
|
|
9310
|
+
managedCredentialsUnavailableDescription:
|
|
9311
|
+
"A conexão do Calendar com o Google não está configurada para esta implantação. Tente novamente mais tarde.",
|
|
9312
|
+
},
|
|
9313
|
+
bookingLinks: {
|
|
9314
|
+
availabilityUnavailable:
|
|
9315
|
+
"Não é possível verificar a disponibilidade do calendário deste anfitrião agora. Tente novamente mais tarde.",
|
|
9316
|
+
},
|
|
9317
|
+
},
|
|
9318
|
+
"hi-IN": {
|
|
9319
|
+
googleConnect: {
|
|
9320
|
+
managedCredentialsUnavailable: "Google Calendar उपलब्ध नहीं है",
|
|
9321
|
+
managedCredentialsUnavailableDescription:
|
|
9322
|
+
"इस deployment के लिए Calendar का Google connection configured नहीं है। कृपया बाद में फिर कोशिश करें।",
|
|
9323
|
+
},
|
|
9324
|
+
bookingLinks: {
|
|
9325
|
+
availabilityUnavailable:
|
|
9326
|
+
"इस host की calendar availability अभी check नहीं की जा सकती। कृपया बाद में फिर कोशिश करें।",
|
|
9327
|
+
},
|
|
9328
|
+
},
|
|
9329
|
+
"ar-SA": {
|
|
9330
|
+
googleConnect: {
|
|
9331
|
+
managedCredentialsUnavailable: "Google Calendar غير متاح",
|
|
9332
|
+
managedCredentialsUnavailableDescription:
|
|
9333
|
+
"اتصال Google في Calendar غير مهيأ لهذا النشر. يُرجى المحاولة لاحقًا.",
|
|
9334
|
+
},
|
|
9335
|
+
bookingLinks: {
|
|
9336
|
+
availabilityUnavailable:
|
|
9337
|
+
"لا يمكن التحقق من توفر تقويم هذا المضيف الآن. يُرجى المحاولة لاحقًا.",
|
|
9338
|
+
},
|
|
9339
|
+
},
|
|
9340
|
+
} satisfies Partial<Record<LocaleCode, PartialMessages>>;
|
|
9341
|
+
|
|
9236
9342
|
function applyTranslatedCalendarOverrides(
|
|
9237
9343
|
translationSet: Partial<Record<LocaleCode, PartialMessages>>,
|
|
9238
9344
|
) {
|
|
@@ -9282,3 +9388,4 @@ applyTranslatedCalendarOverrides(translatedCalendarDebtTranslations);
|
|
|
9282
9388
|
applyTranslatedCalendarOverrides(translatedCalendarRemainingRaw);
|
|
9283
9389
|
applyTranslatedCalendarOverrides(translatedCalendarRawBurnDown);
|
|
9284
9390
|
applyTranslatedCalendarOverrides(translatedCalendarExactCleanup);
|
|
9391
|
+
applyTranslatedCalendarOverrides(translatedCalendarAvailabilityFix);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function localHostname(hostname: string): boolean {
|
|
2
|
+
return (
|
|
3
|
+
hostname === "localhost" ||
|
|
4
|
+
hostname === "127.0.0.1" ||
|
|
5
|
+
hostname === "::1" ||
|
|
6
|
+
hostname.endsWith(".localhost") ||
|
|
7
|
+
hostname.endsWith(".local")
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shouldOfferGoogleOAuthSetup(): boolean {
|
|
12
|
+
const env = (import.meta.env ?? {}) as Record<string, unknown>;
|
|
13
|
+
if (env.DEV === true) return true;
|
|
14
|
+
if (env.VITE_ENABLE_GOOGLE_OAUTH_SETUP === "true") return true;
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
return localHostname(window.location.hostname.toLowerCase());
|
|
17
|
+
}
|
|
@@ -135,6 +135,7 @@ const BRAND_ICON_LINK_CLASS =
|
|
|
135
135
|
const BRAND_PILL_LINK_CLASS =
|
|
136
136
|
"border-[#00B5FF]/35 bg-[#00B5FF]/10 font-semibold text-[#00B5FF] hover:border-[#00B5FF]/55 hover:bg-[#00B5FF]/15 hover:text-[#33C4FF]";
|
|
137
137
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
138
|
+
const BOOKING_SLOT_STEP_MINUTES = 30;
|
|
138
139
|
|
|
139
140
|
type DraftLink = {
|
|
140
141
|
id?: string;
|
|
@@ -1993,7 +1994,14 @@ function BookingPreview({
|
|
|
1993
1994
|
const startMin = startH * 60 + startM;
|
|
1994
1995
|
const endMin = endH * 60 + endM;
|
|
1995
1996
|
const slots: string[] = [];
|
|
1996
|
-
|
|
1997
|
+
const firstStart =
|
|
1998
|
+
Math.ceil(startMin / BOOKING_SLOT_STEP_MINUTES) *
|
|
1999
|
+
BOOKING_SLOT_STEP_MINUTES;
|
|
2000
|
+
for (
|
|
2001
|
+
let m = firstStart;
|
|
2002
|
+
m + dur <= endMin;
|
|
2003
|
+
m += BOOKING_SLOT_STEP_MINUTES
|
|
2004
|
+
) {
|
|
1997
2005
|
const h = Math.floor(m / 60);
|
|
1998
2006
|
const mm = m % 60;
|
|
1999
2007
|
const ampm = h >= 12 ? "PM" : "AM";
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
useT,
|
|
6
6
|
} from "@agent-native/core/client";
|
|
7
7
|
import type { Booking } from "@shared/api";
|
|
8
|
-
import { IconCalendar } from "@tabler/icons-react";
|
|
8
|
+
import { IconAlertTriangle, IconCalendar } from "@tabler/icons-react";
|
|
9
9
|
import {
|
|
10
10
|
addMinutes,
|
|
11
11
|
endOfMonth,
|
|
@@ -122,23 +122,26 @@ export default function BookingPage() {
|
|
|
122
122
|
availability?.slotDurationMinutes ??
|
|
123
123
|
settings?.defaultEventDuration ??
|
|
124
124
|
30;
|
|
125
|
-
const {
|
|
126
|
-
|
|
125
|
+
const {
|
|
126
|
+
data: slots = [],
|
|
127
|
+
isLoading: slotsLoading,
|
|
128
|
+
error: slotsError,
|
|
129
|
+
} = useAvailableSlots(dateStr, duration, slug);
|
|
130
|
+
const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
|
|
131
|
+
const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
|
|
132
|
+
const {
|
|
133
|
+
data: availableDates = [],
|
|
134
|
+
isLoading: availableDatesLoading,
|
|
135
|
+
error: availableDatesError,
|
|
136
|
+
} = useAvailableDays(
|
|
137
|
+
monthStart,
|
|
138
|
+
monthEnd,
|
|
127
139
|
duration,
|
|
128
140
|
slug,
|
|
141
|
+
step === "date" &&
|
|
142
|
+
!!availability &&
|
|
143
|
+
(!hasDurationChoice || selectedDuration !== null),
|
|
129
144
|
);
|
|
130
|
-
const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
|
|
131
|
-
const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
|
|
132
|
-
const { data: availableDates = [], isLoading: availableDatesLoading } =
|
|
133
|
-
useAvailableDays(
|
|
134
|
-
monthStart,
|
|
135
|
-
monthEnd,
|
|
136
|
-
duration,
|
|
137
|
-
slug,
|
|
138
|
-
step === "date" &&
|
|
139
|
-
!!availability &&
|
|
140
|
-
(!hasDurationChoice || selectedDuration !== null),
|
|
141
|
-
);
|
|
142
145
|
const createBooking = useCreateBooking();
|
|
143
146
|
const selectedSlotRange = selectedSlot
|
|
144
147
|
? {
|
|
@@ -236,6 +239,7 @@ export default function BookingPage() {
|
|
|
236
239
|
const pageTitle = bookingLink?.title || title;
|
|
237
240
|
const pageDescription = bookingLink?.description || description;
|
|
238
241
|
const requiredHostCount = (bookingLink?.hosts?.length ?? 0) + 1;
|
|
242
|
+
const availabilityErrorMessage = t("bookingLinks.availabilityUnavailable");
|
|
239
243
|
|
|
240
244
|
useEffect(() => {
|
|
241
245
|
if (hasDurationChoice && step === "date" && selectedDuration === null) {
|
|
@@ -404,17 +408,26 @@ export default function BookingPage() {
|
|
|
404
408
|
<h3 className="mb-4 text-sm font-medium text-center">
|
|
405
409
|
{t("bookingLinks.selectDate")}
|
|
406
410
|
</h3>
|
|
407
|
-
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
{availableDatesError ? (
|
|
412
|
+
<div className="rounded-lg border border-destructive/30 bg-destructive/[0.06] px-3 py-3 text-sm text-destructive">
|
|
413
|
+
<div className="flex items-start gap-2">
|
|
414
|
+
<IconAlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
|
415
|
+
<p>{availabilityErrorMessage}</p>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
) : (
|
|
419
|
+
<div className="flex justify-center">
|
|
420
|
+
<DatePicker
|
|
421
|
+
selectedDate={selectedDate}
|
|
422
|
+
onSelect={handleDateSelect}
|
|
423
|
+
availability={availability}
|
|
424
|
+
availableDates={availableDates}
|
|
425
|
+
availabilityLoading={availableDatesLoading}
|
|
426
|
+
viewMonth={viewMonth}
|
|
427
|
+
onViewMonthChange={setViewMonth}
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
418
431
|
</div>
|
|
419
432
|
)}
|
|
420
433
|
|
|
@@ -443,6 +456,7 @@ export default function BookingPage() {
|
|
|
443
456
|
selectedSlot={selectedSlot}
|
|
444
457
|
onSelect={handleSlotSelect}
|
|
445
458
|
loading={slotsLoading}
|
|
459
|
+
errorMessage={slotsError ? availabilityErrorMessage : undefined}
|
|
446
460
|
/>
|
|
447
461
|
</div>
|
|
448
462
|
)}
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
useDisconnectZoom,
|
|
48
48
|
useZoomStatus,
|
|
49
49
|
} from "@/hooks/use-zoom-auth";
|
|
50
|
+
import { shouldOfferGoogleOAuthSetup } from "@/lib/google-oauth-setup";
|
|
50
51
|
|
|
51
52
|
import changelog from "../../CHANGELOG.md?raw";
|
|
52
53
|
|
|
@@ -70,6 +71,7 @@ export default function Settings() {
|
|
|
70
71
|
const disconnectZoom = useDisconnectZoom();
|
|
71
72
|
const [wantAuthUrl, setWantAuthUrl] = useState(false);
|
|
72
73
|
const authUrl = useGoogleAuthUrl(wantAuthUrl);
|
|
74
|
+
const canOfferGoogleOAuthSetup = shouldOfferGoogleOAuthSetup();
|
|
73
75
|
|
|
74
76
|
const [timezone, setTimezone] = useState("");
|
|
75
77
|
const [bookingTitle, setBookingTitle] = useState("");
|
|
@@ -333,7 +335,7 @@ export default function Settings() {
|
|
|
333
335
|
</Card>
|
|
334
336
|
|
|
335
337
|
{/* Google Setup Wizard */}
|
|
336
|
-
{!googleStatus.data?.connected && (
|
|
338
|
+
{!googleStatus.data?.connected && canOfferGoogleOAuthSetup && (
|
|
337
339
|
<Card>
|
|
338
340
|
<CardHeader>
|
|
339
341
|
<CardTitle className="text-lg">
|