@agent-native/core 0.68.1 → 0.68.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/docs/content/authentication.md +16 -2
- package/corpus/core/docs/content/template-analytics.md +14 -9
- package/corpus/core/docs/content/template-clips.md +30 -2
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/client/CommandMenu.tsx +98 -3
- package/corpus/core/src/client/ConnectBuilderCard.tsx +11 -2
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/oauth-tokens/google-refresh.ts +32 -17
- package/corpus/core/src/provider-api/index.ts +37 -17
- package/corpus/core/src/server/auth.ts +6 -8
- package/corpus/core/src/server/better-auth-instance.ts +19 -4
- package/corpus/core/src/server/core-routes-plugin.ts +157 -5
- package/corpus/core/src/server/google-oauth-credentials.ts +51 -0
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/server/onboarding-html.ts +2 -1
- package/corpus/templates/analytics/app/routes/$.tsx +9 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +33 -1
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +11 -3
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +25 -2
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +12 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +7 -2
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +37 -78
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +33 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +46 -3
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +203 -2
- package/corpus/templates/calendar/app/lib/event-colors.ts +1 -4
- package/corpus/templates/calendar/app/lib/rsvp-status.tsx +6 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +9 -4
- package/corpus/templates/calendar/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/calendar/app/pages/Settings.tsx +25 -1
- package/corpus/templates/calendar/app/routes/_app.$.tsx +9 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +152 -12
- package/corpus/templates/calendar/server/lib/google-calendar.ts +63 -14
- package/corpus/templates/calendar/server/plugins/auth.ts +4 -20
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +17 -0
- package/corpus/templates/clips/actions/sync-calendars.ts +6 -7
- package/corpus/templates/clips/app/components/capture-install-options.tsx +157 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +9 -8
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +4 -6
- package/corpus/templates/clips/app/lib/capture-install-options.ts +13 -0
- package/corpus/templates/clips/app/root.tsx +54 -2
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +9 -13
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -7
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +5 -7
- package/corpus/templates/clips/app/routes/download.tsx +49 -0
- package/corpus/templates/clips/app/routes/record.tsx +4 -4
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -6
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +26 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -7
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +61 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +19 -12
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +6 -6
- package/corpus/templates/design/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/design/app/routes/$.tsx +9 -0
- package/corpus/templates/mail/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/mail/app/routes/$.tsx +9 -0
- package/corpus/templates/plan/app/global.css +2 -2
- package/corpus/templates/slides/app/pages/NotFound.tsx +1 -1
- package/corpus/templates/slides/app/routes/$.tsx +9 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +81 -25
- package/corpus/templates/videos/app/pages/NotFound.tsx +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +15 -2
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +46 -3
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +11 -2
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/oauth-tokens/google-refresh.d.ts.map +1 -1
- package/dist/oauth-tokens/google-refresh.js +29 -17
- package/dist/oauth-tokens/google-refresh.js.map +1 -1
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -16
- package/dist/provider-api/index.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +6 -6
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +18 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +5 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +107 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth-credentials.d.ts +15 -0
- package/dist/server/google-oauth-credentials.d.ts.map +1 -0
- package/dist/server/google-oauth-credentials.js +34 -0
- package/dist/server/google-oauth-credentials.js.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +2 -1
- package/dist/server/onboarding-html.js.map +1 -1
- package/docs/content/authentication.md +16 -2
- package/docs/content/template-analytics.md +14 -9
- package/docs/content/template-clips.md +30 -2
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ export function EventCard({
|
|
|
30
30
|
colorPreferences,
|
|
31
31
|
}: EventCardProps) {
|
|
32
32
|
const accentColor = getEventDisplayColor(event, colorPreferences);
|
|
33
|
+
const ownerLabel = event.ownerName || event.overlayEmail;
|
|
33
34
|
|
|
34
35
|
const handleDragStart = (e: React.DragEvent) => {
|
|
35
36
|
e.dataTransfer.setData("text/plain", event.id);
|
|
@@ -47,10 +48,14 @@ export function EventCard({
|
|
|
47
48
|
onDragStart={canDrag ? handleDragStart : undefined}
|
|
48
49
|
onDragEnd={canDrag ? onDragEnd : undefined}
|
|
49
50
|
className={cn(
|
|
50
|
-
"flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-xs text-foreground transition-all hover:brightness-110",
|
|
51
|
+
"relative flex w-full items-center gap-1.5 rounded px-1.5 py-0.5 text-left text-xs text-foreground transition-all hover:brightness-110",
|
|
51
52
|
canDrag && "cursor-grab active:cursor-grabbing",
|
|
52
53
|
dimmed && "opacity-40",
|
|
54
|
+
event.ownerColor && "pr-3.5",
|
|
53
55
|
)}
|
|
56
|
+
aria-label={
|
|
57
|
+
ownerLabel ? `${event.title}, ${ownerLabel}'s calendar` : event.title
|
|
58
|
+
}
|
|
54
59
|
style={{
|
|
55
60
|
backgroundColor: `${accentColor}25`,
|
|
56
61
|
}}
|
|
@@ -68,6 +73,13 @@ export function EventCard({
|
|
|
68
73
|
)}
|
|
69
74
|
<EventStatusIcon event={event} />
|
|
70
75
|
<span className="truncate font-medium">{event.title}</span>
|
|
76
|
+
{event.ownerColor && (
|
|
77
|
+
<span
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
|
|
80
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
71
83
|
</button>
|
|
72
84
|
);
|
|
73
85
|
}
|
|
@@ -79,10 +91,14 @@ export function EventCard({
|
|
|
79
91
|
onDragStart={canDrag ? handleDragStart : undefined}
|
|
80
92
|
onDragEnd={canDrag ? onDragEnd : undefined}
|
|
81
93
|
className={cn(
|
|
82
|
-
"flex w-full flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-foreground transition-all hover:brightness-110",
|
|
94
|
+
"relative flex w-full flex-col gap-0.5 rounded-md px-2 py-1.5 text-left text-xs text-foreground transition-all hover:brightness-110",
|
|
83
95
|
canDrag && "cursor-grab active:cursor-grabbing",
|
|
84
96
|
dimmed && "opacity-40",
|
|
97
|
+
event.ownerColor && "pr-4",
|
|
85
98
|
)}
|
|
99
|
+
aria-label={
|
|
100
|
+
ownerLabel ? `${event.title}, ${ownerLabel}'s calendar` : event.title
|
|
101
|
+
}
|
|
86
102
|
style={{
|
|
87
103
|
backgroundColor: `${accentColor}25`,
|
|
88
104
|
borderLeft: `2px solid ${accentColor}`,
|
|
@@ -106,6 +122,13 @@ export function EventCard({
|
|
|
106
122
|
})}
|
|
107
123
|
</span>
|
|
108
124
|
)}
|
|
125
|
+
{event.ownerColor && (
|
|
126
|
+
<span
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
|
|
129
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
109
132
|
</button>
|
|
110
133
|
);
|
|
111
134
|
}
|
|
@@ -101,6 +101,7 @@ export function EventDetailPanel({
|
|
|
101
101
|
const isOverlay = !!event?.overlayEmail;
|
|
102
102
|
const lastSavedDescriptionRef = useRef(event?.description || "");
|
|
103
103
|
const meetingLink = event ? extractMeetingLink(event) : null;
|
|
104
|
+
const ownerLabel = event?.ownerName || event?.overlayEmail;
|
|
104
105
|
|
|
105
106
|
// Reset editing state when event changes
|
|
106
107
|
useEffect(() => {
|
|
@@ -310,6 +311,17 @@ export function EventDetailPanel({
|
|
|
310
311
|
</div>
|
|
311
312
|
)}
|
|
312
313
|
|
|
314
|
+
{event.overlayEmail && ownerLabel && (
|
|
315
|
+
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
|
316
|
+
<span
|
|
317
|
+
aria-hidden="true"
|
|
318
|
+
className="ml-0.5 size-2 shrink-0 rounded-full ring-1 ring-border"
|
|
319
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
320
|
+
/>
|
|
321
|
+
<span>Viewing {ownerLabel}'s calendar</span>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
313
325
|
{meetingLink ? (
|
|
314
326
|
<a
|
|
315
327
|
href={safeUrl(meetingLink)}
|
|
@@ -395,6 +395,7 @@ export function EventDetailPopover({
|
|
|
395
395
|
"meet" | "zoom" | null
|
|
396
396
|
>(null);
|
|
397
397
|
const isOverlay = !!event.overlayEmail;
|
|
398
|
+
const ownerLabel = event.ownerName || event.overlayEmail;
|
|
398
399
|
|
|
399
400
|
const updateEvent = useUpdateEvent();
|
|
400
401
|
const masterEventId =
|
|
@@ -2086,9 +2087,13 @@ Write a short, useful meeting description. If I ask you to apply it, update this
|
|
|
2086
2087
|
<>
|
|
2087
2088
|
<div className="mx-4 my-2 border-t border-border/50" />
|
|
2088
2089
|
<div className="flex items-center gap-3 px-4 py-1.5">
|
|
2089
|
-
<
|
|
2090
|
+
<span
|
|
2091
|
+
aria-hidden="true"
|
|
2092
|
+
className="ml-1 size-2 shrink-0 rounded-full ring-1 ring-border"
|
|
2093
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
2094
|
+
/>
|
|
2090
2095
|
<span className="text-sm text-muted-foreground">
|
|
2091
|
-
{
|
|
2096
|
+
Viewing {ownerLabel}'s calendar
|
|
2092
2097
|
</span>
|
|
2093
2098
|
</div>
|
|
2094
2099
|
</>
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
useGoogleAuthStatus,
|
|
22
22
|
useGoogleAuthUrl,
|
|
23
23
|
useGoogleAddAccountUrl,
|
|
24
|
+
useGoogleDesktopAuth,
|
|
24
25
|
useDisconnectGoogle,
|
|
26
|
+
type DesktopAuthIssue,
|
|
25
27
|
} from "@/hooks/use-google-auth";
|
|
26
28
|
|
|
27
29
|
interface EnvKeyStatus {
|
|
@@ -66,15 +68,6 @@ interface GoogleConnectBannerProps {
|
|
|
66
68
|
variant?: "banner" | "hero";
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
interface DesktopAuthIssue {
|
|
70
|
-
error?: string;
|
|
71
|
-
message?: string;
|
|
72
|
-
code?: string;
|
|
73
|
-
accountId?: string;
|
|
74
|
-
existingOwner?: string;
|
|
75
|
-
attemptedOwner?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
71
|
export function GoogleConnectBanner({
|
|
79
72
|
variant = "banner",
|
|
80
73
|
}: GoogleConnectBannerProps) {
|
|
@@ -93,76 +86,23 @@ export function GoogleConnectBanner({
|
|
|
93
86
|
const hasAccounts = accounts.length > 0;
|
|
94
87
|
|
|
95
88
|
const isBuilderFrame = useMemo(() => isInBuilderFrame(), []);
|
|
96
|
-
const useDesktopAuth = useMemo(
|
|
97
|
-
() => /AgentNativeDesktop/i.test(navigator.userAgent) && !isBuilderFrame,
|
|
98
|
-
[isBuilderFrame],
|
|
99
|
-
);
|
|
100
|
-
const desktopPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
101
89
|
const authPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
102
90
|
const addAccountPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
91
|
+
const {
|
|
92
|
+
isDesktopGoogleAuth,
|
|
93
|
+
isGoogleDesktopAuthPending,
|
|
94
|
+
startDesktopGoogleAuth,
|
|
95
|
+
} = useGoogleDesktopAuth({
|
|
96
|
+
onError: setDesktopAuthIssue,
|
|
97
|
+
onSuccess: () => window.location.reload(),
|
|
98
|
+
});
|
|
103
99
|
useEffect(() => {
|
|
104
100
|
return () => {
|
|
105
|
-
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
106
101
|
if (authPollRef.current) clearInterval(authPollRef.current);
|
|
107
102
|
if (addAccountPollRef.current) clearInterval(addAccountPollRef.current);
|
|
108
103
|
};
|
|
109
104
|
}, []);
|
|
110
105
|
|
|
111
|
-
function signInViaDesktopBrowser(addAccount = false) {
|
|
112
|
-
setDesktopAuthIssue(null);
|
|
113
|
-
const flowId =
|
|
114
|
-
crypto.randomUUID?.() ||
|
|
115
|
-
Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
116
|
-
const origin = window.location.origin;
|
|
117
|
-
const endpoint = addAccount
|
|
118
|
-
? "/_agent-native/google/add-account/auth-url"
|
|
119
|
-
: "/_agent-native/google/auth-url";
|
|
120
|
-
const redirectUri = encodeURIComponent(
|
|
121
|
-
oauthRedirectUri("/_agent-native/google/callback"),
|
|
122
|
-
);
|
|
123
|
-
window.open(
|
|
124
|
-
`${origin}${agentNativePath(endpoint)}?redirect_uri=${redirectUri}&desktop=1&flow_id=${flowId}&redirect=1`,
|
|
125
|
-
"_blank",
|
|
126
|
-
);
|
|
127
|
-
const start = Date.now();
|
|
128
|
-
if (desktopPollRef.current) clearInterval(desktopPollRef.current);
|
|
129
|
-
desktopPollRef.current = setInterval(async () => {
|
|
130
|
-
try {
|
|
131
|
-
const res = await fetch(
|
|
132
|
-
agentNativePath(
|
|
133
|
-
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
134
|
-
),
|
|
135
|
-
);
|
|
136
|
-
const data = await res.json();
|
|
137
|
-
if (data?.error) {
|
|
138
|
-
clearInterval(desktopPollRef.current!);
|
|
139
|
-
desktopPollRef.current = null;
|
|
140
|
-
setDesktopAuthIssue(data);
|
|
141
|
-
} else if (data?.token) {
|
|
142
|
-
clearInterval(desktopPollRef.current!);
|
|
143
|
-
desktopPollRef.current = null;
|
|
144
|
-
await fetch(
|
|
145
|
-
agentNativePath(
|
|
146
|
-
`/_agent-native/auth/session?_session=${data.token}`,
|
|
147
|
-
),
|
|
148
|
-
{
|
|
149
|
-
credentials: "include",
|
|
150
|
-
},
|
|
151
|
-
);
|
|
152
|
-
window.location.reload();
|
|
153
|
-
} else if (Date.now() - start > 120_000) {
|
|
154
|
-
clearInterval(desktopPollRef.current!);
|
|
155
|
-
desktopPollRef.current = null;
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
if (Date.now() - start > 120_000) {
|
|
159
|
-
clearInterval(desktopPollRef.current!);
|
|
160
|
-
desktopPollRef.current = null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}, 1500);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
106
|
// Wizard state
|
|
167
107
|
const [currentStep, setCurrentStep] = useState(0);
|
|
168
108
|
const [saving, setSaving] = useState(false);
|
|
@@ -293,16 +233,19 @@ export function GoogleConnectBanner({
|
|
|
293
233
|
|
|
294
234
|
function handleConnect() {
|
|
295
235
|
setDesktopAuthIssue(null);
|
|
296
|
-
if (
|
|
297
|
-
|
|
236
|
+
if (isDesktopGoogleAuth) {
|
|
237
|
+
startDesktopGoogleAuth({ previousAccountCount: accounts.length });
|
|
298
238
|
return;
|
|
299
239
|
}
|
|
300
240
|
setWantAuthUrl(true);
|
|
301
241
|
}
|
|
302
242
|
|
|
303
243
|
function handleAddAccount() {
|
|
304
|
-
if (
|
|
305
|
-
|
|
244
|
+
if (isDesktopGoogleAuth) {
|
|
245
|
+
startDesktopGoogleAuth({
|
|
246
|
+
addAccount: true,
|
|
247
|
+
previousAccountCount: accounts.length,
|
|
248
|
+
});
|
|
306
249
|
return;
|
|
307
250
|
}
|
|
308
251
|
setWantAddAccount(true);
|
|
@@ -377,7 +320,11 @@ export function GoogleConnectBanner({
|
|
|
377
320
|
size="sm"
|
|
378
321
|
className="mt-6 gap-2 px-4 h-8 text-[13px] font-medium"
|
|
379
322
|
onClick={handleConnect}
|
|
380
|
-
disabled={
|
|
323
|
+
disabled={
|
|
324
|
+
authUrl.isLoading ||
|
|
325
|
+
authUrl.isFetching ||
|
|
326
|
+
isGoogleDesktopAuthPending
|
|
327
|
+
}
|
|
381
328
|
>
|
|
382
329
|
<GoogleIcon className="h-3.5 w-3.5" />
|
|
383
330
|
{authUrl.isLoading
|
|
@@ -458,7 +405,11 @@ export function GoogleConnectBanner({
|
|
|
458
405
|
))}
|
|
459
406
|
<button
|
|
460
407
|
onClick={handleAddAccount}
|
|
461
|
-
disabled={
|
|
408
|
+
disabled={
|
|
409
|
+
addAccountUrl.isLoading ||
|
|
410
|
+
addAccountUrl.isFetching ||
|
|
411
|
+
isGoogleDesktopAuthPending
|
|
412
|
+
}
|
|
462
413
|
className="text-xs text-foreground/40 hover:text-foreground/60 transition-colors whitespace-nowrap"
|
|
463
414
|
>
|
|
464
415
|
+ Add account
|
|
@@ -515,7 +466,11 @@ export function GoogleConnectBanner({
|
|
|
515
466
|
size="sm"
|
|
516
467
|
className="gap-1.5 text-xs h-7 font-medium bg-white text-black hover:bg-white/90"
|
|
517
468
|
onClick={handleConnect}
|
|
518
|
-
disabled={
|
|
469
|
+
disabled={
|
|
470
|
+
authUrl.isLoading ||
|
|
471
|
+
authUrl.isFetching ||
|
|
472
|
+
isGoogleDesktopAuthPending
|
|
473
|
+
}
|
|
519
474
|
>
|
|
520
475
|
<GoogleIcon className="h-3 w-3" />
|
|
521
476
|
{authUrl.isLoading ? "Connecting..." : "Sign in with Google"}
|
|
@@ -526,7 +481,11 @@ export function GoogleConnectBanner({
|
|
|
526
481
|
variant="outline"
|
|
527
482
|
className="gap-1.5 text-xs h-7 font-medium"
|
|
528
483
|
onClick={handleConnect}
|
|
529
|
-
disabled={
|
|
484
|
+
disabled={
|
|
485
|
+
authUrl.isLoading ||
|
|
486
|
+
authUrl.isFetching ||
|
|
487
|
+
isGoogleDesktopAuthPending
|
|
488
|
+
}
|
|
530
489
|
>
|
|
531
490
|
{authUrl.isLoading ? "..." : "Connect Google"}
|
|
532
491
|
</Button>
|
|
@@ -592,7 +592,17 @@ export function WeekView({
|
|
|
592
592
|
onDraftDiscard={onDraftDiscard}
|
|
593
593
|
>
|
|
594
594
|
<button
|
|
595
|
-
className=
|
|
595
|
+
className={cn(
|
|
596
|
+
"absolute flex items-center gap-1 truncate rounded px-1.5 text-left text-[11px] font-medium text-foreground transition-opacity hover:opacity-80",
|
|
597
|
+
event.ownerColor && "pr-3.5",
|
|
598
|
+
)}
|
|
599
|
+
aria-label={
|
|
600
|
+
event.ownerName || event.overlayEmail
|
|
601
|
+
? `${event.title}, ${
|
|
602
|
+
event.ownerName || event.overlayEmail
|
|
603
|
+
}'s calendar`
|
|
604
|
+
: event.title
|
|
605
|
+
}
|
|
596
606
|
style={{
|
|
597
607
|
top: `${rowIdx * allDayRowHeight + 4}px`,
|
|
598
608
|
left: `${leftPct}%`,
|
|
@@ -613,6 +623,13 @@ export function WeekView({
|
|
|
613
623
|
)}
|
|
614
624
|
<EventStatusIcon event={event} className="shrink-0" />
|
|
615
625
|
<span className="truncate">{event.title}</span>
|
|
626
|
+
{event.ownerColor && (
|
|
627
|
+
<span
|
|
628
|
+
aria-hidden="true"
|
|
629
|
+
className="absolute right-1 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
|
|
630
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
631
|
+
/>
|
|
632
|
+
)}
|
|
616
633
|
</button>
|
|
617
634
|
</EventDetailPopover>
|
|
618
635
|
);
|
|
@@ -864,7 +881,15 @@ export function WeekView({
|
|
|
864
881
|
"ring-2 ring-primary/40",
|
|
865
882
|
canDrag && segmentStartsHere && "cursor-grab",
|
|
866
883
|
isBeingDragged && isDragging && "cursor-grabbing",
|
|
884
|
+
event.ownerColor && "pr-4",
|
|
867
885
|
)}
|
|
886
|
+
aria-label={
|
|
887
|
+
event.ownerName || event.overlayEmail
|
|
888
|
+
? `${event.title}, ${
|
|
889
|
+
event.ownerName || event.overlayEmail
|
|
890
|
+
}'s calendar`
|
|
891
|
+
: event.title
|
|
892
|
+
}
|
|
868
893
|
style={{
|
|
869
894
|
...style,
|
|
870
895
|
left: `${li.left}px`,
|
|
@@ -894,6 +919,13 @@ export function WeekView({
|
|
|
894
919
|
isBeingDragged && isDragging ? 0.9 : undefined,
|
|
895
920
|
}}
|
|
896
921
|
>
|
|
922
|
+
{event.ownerColor && (
|
|
923
|
+
<span
|
|
924
|
+
aria-hidden="true"
|
|
925
|
+
className="absolute right-1.5 top-1.5 size-1.5 rounded-full ring-1 ring-background/70"
|
|
926
|
+
style={{ backgroundColor: event.ownerColor }}
|
|
927
|
+
/>
|
|
928
|
+
)}
|
|
897
929
|
{durationMin <= 30 ? (
|
|
898
930
|
<div className="flex items-baseline gap-1 truncate">
|
|
899
931
|
{allOthersOut && (
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
useGoogleAuthStatus,
|
|
47
47
|
useGoogleAuthUrl,
|
|
48
48
|
useGoogleAddAccountUrl,
|
|
49
|
+
useGoogleDesktopAuth,
|
|
49
50
|
} from "@/hooks/use-google-auth";
|
|
50
51
|
import {
|
|
51
52
|
appPath,
|
|
@@ -305,6 +306,13 @@ function MiniCalendar({
|
|
|
305
306
|
function GoogleConnectSidebarButton() {
|
|
306
307
|
const [wantAuthUrl, setWantAuthUrl] = useState(false);
|
|
307
308
|
const authUrl = useGoogleAuthUrl(wantAuthUrl);
|
|
309
|
+
const {
|
|
310
|
+
isDesktopGoogleAuth,
|
|
311
|
+
isGoogleDesktopAuthPending,
|
|
312
|
+
startDesktopGoogleAuth,
|
|
313
|
+
} = useGoogleDesktopAuth({
|
|
314
|
+
onSuccess: () => window.location.reload(),
|
|
315
|
+
});
|
|
308
316
|
|
|
309
317
|
useEffect(() => {
|
|
310
318
|
if (!wantAuthUrl || !authUrl.data?.url) return;
|
|
@@ -312,6 +320,14 @@ function GoogleConnectSidebarButton() {
|
|
|
312
320
|
window.open(authUrl.data.url, "_blank");
|
|
313
321
|
}, [wantAuthUrl, authUrl.data]);
|
|
314
322
|
|
|
323
|
+
function handleConnect() {
|
|
324
|
+
if (isDesktopGoogleAuth) {
|
|
325
|
+
startDesktopGoogleAuth({ previousAccountCount: 0 });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
setWantAuthUrl(true);
|
|
329
|
+
}
|
|
330
|
+
|
|
315
331
|
return (
|
|
316
332
|
<div className="border-t border-border p-3">
|
|
317
333
|
<div className="rounded-lg bg-primary/10 p-3">
|
|
@@ -324,8 +340,12 @@ function GoogleConnectSidebarButton() {
|
|
|
324
340
|
<Button
|
|
325
341
|
size="sm"
|
|
326
342
|
className="w-full gap-1.5 text-xs font-semibold"
|
|
327
|
-
onClick={
|
|
328
|
-
disabled={
|
|
343
|
+
onClick={handleConnect}
|
|
344
|
+
disabled={
|
|
345
|
+
authUrl.isLoading ||
|
|
346
|
+
authUrl.isFetching ||
|
|
347
|
+
isGoogleDesktopAuthPending
|
|
348
|
+
}
|
|
329
349
|
>
|
|
330
350
|
<IconExternalLink className="h-3 w-3" />
|
|
331
351
|
{authUrl.isLoading ? "Connecting..." : "Connect"}
|
|
@@ -398,6 +418,13 @@ function GoogleAccountsSection({
|
|
|
398
418
|
} = useViewPreferences();
|
|
399
419
|
const [wantAddAccount, setWantAddAccount] = useState(false);
|
|
400
420
|
const addAccountUrl = useGoogleAddAccountUrl(wantAddAccount);
|
|
421
|
+
const {
|
|
422
|
+
isDesktopGoogleAuth,
|
|
423
|
+
isGoogleDesktopAuthPending,
|
|
424
|
+
startDesktopGoogleAuth,
|
|
425
|
+
} = useGoogleDesktopAuth({
|
|
426
|
+
onSuccess: () => window.location.reload(),
|
|
427
|
+
});
|
|
401
428
|
|
|
402
429
|
useEffect(() => {
|
|
403
430
|
if (!wantAddAccount || !addAccountUrl.data?.url) return;
|
|
@@ -405,6 +432,17 @@ function GoogleAccountsSection({
|
|
|
405
432
|
setWantAddAccount(false);
|
|
406
433
|
}, [wantAddAccount, addAccountUrl.data]);
|
|
407
434
|
|
|
435
|
+
function handleAddAccount() {
|
|
436
|
+
if (isDesktopGoogleAuth) {
|
|
437
|
+
startDesktopGoogleAuth({
|
|
438
|
+
addAccount: true,
|
|
439
|
+
previousAccountCount: accounts.length,
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
setWantAddAccount(true);
|
|
444
|
+
}
|
|
445
|
+
|
|
408
446
|
function handlePickColor(color: string) {
|
|
409
447
|
updateViewPreferences({ colorMode: "single", singleColor: color });
|
|
410
448
|
}
|
|
@@ -438,7 +476,12 @@ function GoogleAccountsSection({
|
|
|
438
476
|
<TooltipTrigger asChild>
|
|
439
477
|
<button
|
|
440
478
|
type="button"
|
|
441
|
-
onClick={
|
|
479
|
+
onClick={handleAddAccount}
|
|
480
|
+
disabled={
|
|
481
|
+
addAccountUrl.isLoading ||
|
|
482
|
+
addAccountUrl.isFetching ||
|
|
483
|
+
isGoogleDesktopAuthPending
|
|
484
|
+
}
|
|
442
485
|
className="flex h-5 w-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
|
|
443
486
|
>
|
|
444
487
|
<IconPlus className="h-3.5 w-3.5" />
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
agentNativePath,
|
|
4
|
+
isInBuilderFrame,
|
|
5
|
+
oauthRedirectUri,
|
|
6
|
+
} from "@agent-native/core/client";
|
|
3
7
|
import type { GoogleAuthStatus } from "@shared/api";
|
|
4
|
-
import { useEffect } from "react";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export interface DesktopAuthIssue {
|
|
11
|
+
error?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
code?: string;
|
|
14
|
+
accountId?: string;
|
|
15
|
+
existingOwner?: string;
|
|
16
|
+
attemptedOwner?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DesktopAuthResult {
|
|
20
|
+
token?: string;
|
|
21
|
+
email?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DesktopAuthStartOptions {
|
|
25
|
+
addAccount?: boolean;
|
|
26
|
+
previousAccountCount?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DesktopAuthOptions {
|
|
30
|
+
onError?: (issue: DesktopAuthIssue) => void;
|
|
31
|
+
onSuccess?: (result: DesktopAuthResult) => void | Promise<void>;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
function bodyError(
|
|
7
36
|
body: any,
|
|
@@ -134,6 +163,178 @@ export function useGoogleAddAccountUrl(enabled = false) {
|
|
|
134
163
|
return query;
|
|
135
164
|
}
|
|
136
165
|
|
|
166
|
+
export function useGoogleDesktopAuth(options: DesktopAuthOptions = {}) {
|
|
167
|
+
const { onError, onSuccess, timeoutMs = 120_000 } = options;
|
|
168
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
169
|
+
const [isPending, setIsPending] = useState(false);
|
|
170
|
+
const isDesktopGoogleAuth = useMemo(() => {
|
|
171
|
+
if (typeof navigator === "undefined") return false;
|
|
172
|
+
return (
|
|
173
|
+
/AgentNativeDesktop/i.test(navigator.userAgent) && !isInBuilderFrame()
|
|
174
|
+
);
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const clearPoll = useCallback(() => {
|
|
178
|
+
if (pollRef.current) {
|
|
179
|
+
clearInterval(pollRef.current);
|
|
180
|
+
pollRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
useEffect(() => clearPoll, [clearPoll]);
|
|
185
|
+
|
|
186
|
+
const startDesktopGoogleAuth = useCallback(
|
|
187
|
+
(startOptions: DesktopAuthStartOptions = {}) => {
|
|
188
|
+
if (!isDesktopGoogleAuth || typeof window === "undefined") return false;
|
|
189
|
+
|
|
190
|
+
clearPoll();
|
|
191
|
+
setIsPending(true);
|
|
192
|
+
const flowId =
|
|
193
|
+
globalThis.crypto?.randomUUID?.() ||
|
|
194
|
+
Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
195
|
+
const redirectUri = oauthRedirectUri("/_agent-native/google/callback");
|
|
196
|
+
const params = new URLSearchParams({
|
|
197
|
+
redirect_uri: redirectUri,
|
|
198
|
+
desktop: "1",
|
|
199
|
+
flow_id: flowId,
|
|
200
|
+
});
|
|
201
|
+
let popup: Window | null = null;
|
|
202
|
+
const reportError = (issue: DesktopAuthIssue) => {
|
|
203
|
+
clearPoll();
|
|
204
|
+
setIsPending(false);
|
|
205
|
+
onError?.(issue);
|
|
206
|
+
};
|
|
207
|
+
const openAuthUrl = (url: string) => {
|
|
208
|
+
if (popup && !popup.closed) {
|
|
209
|
+
popup.location.href = url;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
popup = window.open(url, "_blank");
|
|
213
|
+
return !!popup;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const finish = async (result: DesktopAuthResult = {}) => {
|
|
218
|
+
clearPoll();
|
|
219
|
+
setIsPending(false);
|
|
220
|
+
await onSuccess?.(result);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (startOptions.addAccount) {
|
|
224
|
+
popup = window.open("about:blank", "_blank");
|
|
225
|
+
void (async () => {
|
|
226
|
+
try {
|
|
227
|
+
const { url } = await fetchJson<{ url: string }>(
|
|
228
|
+
agentNativePath(
|
|
229
|
+
`/_agent-native/google/add-account/auth-url?${params.toString()}`,
|
|
230
|
+
),
|
|
231
|
+
{ credentials: "include" },
|
|
232
|
+
);
|
|
233
|
+
if (!openAuthUrl(url)) {
|
|
234
|
+
reportError({
|
|
235
|
+
code: "popup_blocked",
|
|
236
|
+
message:
|
|
237
|
+
"Calendar could not open Google sign-in. Allow popups and try again.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
popup?.close();
|
|
242
|
+
reportError({
|
|
243
|
+
code: "desktop_auth_start_failed",
|
|
244
|
+
message:
|
|
245
|
+
err instanceof Error
|
|
246
|
+
? err.message
|
|
247
|
+
: "Could not start Google sign-in.",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
} else {
|
|
252
|
+
params.set("redirect", "1");
|
|
253
|
+
const opened = openAuthUrl(
|
|
254
|
+
`${window.location.origin}${agentNativePath(
|
|
255
|
+
"/_agent-native/google/auth-url",
|
|
256
|
+
)}?${params.toString()}`,
|
|
257
|
+
);
|
|
258
|
+
if (!opened) {
|
|
259
|
+
reportError({
|
|
260
|
+
code: "popup_blocked",
|
|
261
|
+
message:
|
|
262
|
+
"Calendar could not open Google sign-in. Allow popups and try again.",
|
|
263
|
+
});
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
pollRef.current = setInterval(async () => {
|
|
269
|
+
try {
|
|
270
|
+
const exchangeRes = await fetch(
|
|
271
|
+
agentNativePath(
|
|
272
|
+
`/_agent-native/auth/desktop-exchange?flow_id=${flowId}`,
|
|
273
|
+
),
|
|
274
|
+
{ credentials: "include" },
|
|
275
|
+
);
|
|
276
|
+
const exchange = await exchangeRes.json();
|
|
277
|
+
if (exchange?.error) {
|
|
278
|
+
clearPoll();
|
|
279
|
+
setIsPending(false);
|
|
280
|
+
onError?.(exchange);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (exchange?.token) {
|
|
284
|
+
await fetch(
|
|
285
|
+
agentNativePath(
|
|
286
|
+
`/_agent-native/auth/session?_session=${exchange.token}`,
|
|
287
|
+
),
|
|
288
|
+
{ credentials: "include" },
|
|
289
|
+
);
|
|
290
|
+
await finish({ token: exchange.token, email: exchange.email });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Keep polling; the status endpoint below may still observe success.
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const statusRes = await fetch(
|
|
299
|
+
agentNativePath("/_agent-native/google/status"),
|
|
300
|
+
{ credentials: "include" },
|
|
301
|
+
);
|
|
302
|
+
if (statusRes.ok) {
|
|
303
|
+
const status = (await statusRes.json()) as GoogleAuthStatus;
|
|
304
|
+
const connected = startOptions.addAccount
|
|
305
|
+
? (status.accounts?.length ?? 0) >
|
|
306
|
+
(startOptions.previousAccountCount ?? 0)
|
|
307
|
+
: status.connected;
|
|
308
|
+
if (connected) {
|
|
309
|
+
await finish();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Keep polling until the timeout.
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
318
|
+
reportError({
|
|
319
|
+
code: "desktop_auth_timeout",
|
|
320
|
+
message:
|
|
321
|
+
"Google sign-in timed out. Finish sign-in in the browser or try again.",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}, 1500);
|
|
325
|
+
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
[clearPoll, isDesktopGoogleAuth, onError, onSuccess, timeoutMs],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
isDesktopGoogleAuth,
|
|
333
|
+
isGoogleDesktopAuthPending: isPending,
|
|
334
|
+
startDesktopGoogleAuth,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
137
338
|
export function useDisconnectGoogle() {
|
|
138
339
|
const queryClient = useQueryClient();
|
|
139
340
|
return useMutation({
|