kyd-shared-badge 0.3.78 → 0.3.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.3.78",
3
+ "version": "0.3.79",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -1,9 +1,9 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import { useRouter } from 'next/navigation';
3
3
  import { ProviderIcon } from '../utils/provider';
4
4
  import { normalizeLinkedInInput } from './linkedin';
5
5
  import type { ConnectAccountsProps } from './types';
6
- import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink } from 'lucide-react';
6
+ import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings } from 'lucide-react';
7
7
  import { AnimatePresence, motion } from 'framer-motion';
8
8
  import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
9
9
  import Link from 'next/link';
@@ -38,6 +38,9 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
38
38
  headerDescription,
39
39
  requiredProviders,
40
40
  companyName,
41
+ initialProviderId,
42
+ githubAppSlugId,
43
+ userId,
41
44
  } = props;
42
45
 
43
46
  const router = useRouter();
@@ -45,11 +48,23 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
45
48
  const [linkUrl, setLinkUrl] = useState('');
46
49
  const [isSubmitting, setIsSubmitting] = useState(false);
47
50
  const [isDisconnecting, setIsDisconnecting] = useState<string | null>(null);
51
+ const [showGithubManage, setShowGithubManage] = useState(false);
48
52
 
49
53
  const apiBase = apiGatewayUrl || (typeof process !== 'undefined' ? (process.env.NEXT_PUBLIC_API_GATEWAY_URL as string) : '');
50
- const connectedIds = useMemo(() => new Set((connected || []).map(c => c.id.toLowerCase())), [connected]);
54
+ const connectedIds = useMemo(() => new Set((connected || []).map(c => c.name.toLowerCase())), [connected]);
51
55
  const reconnectIds = useMemo(() => new Set((needsReconnectIds || []).map(id => id.toLowerCase())), [needsReconnectIds]);
52
56
 
57
+ // React to upstream changes to initialProviderId (e.g., after oauth success)
58
+ useEffect(() => {
59
+ if (initialProviderId && initialProviderId !== selectedProviderId) {
60
+ setSelectedProviderIdAndCallback(initialProviderId);
61
+ // If we landed here from initialProviderId, show the existing card (not manage)
62
+ if (initialProviderId === 'githubapp') setShowGithubManage(false);
63
+ }
64
+ // Do not clear selection if initialProviderId becomes falsy later
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [initialProviderId]);
67
+
53
68
  const list = useMemo(() => {
54
69
  const arr = [...providers];
55
70
  if (!sort) return arr;
@@ -94,6 +109,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
94
109
  });
95
110
  const data = await res.json().catch(() => ({}));
96
111
  if (!res.ok) throw new Error(data?.error || `Failed to disconnect ${providerId}.`);
112
+ setShowGithubManage(false);
97
113
  if (onDisconnected) onDisconnected(providerId);
98
114
  } catch (e) {
99
115
  const err = e as Error;
@@ -156,6 +172,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
156
172
  const handleConnectBack = () => {
157
173
  setSelectedProviderIdAndCallback(null);
158
174
  setLinkUrl('');
175
+ setShowGithubManage(false);
159
176
  };
160
177
 
161
178
  const cardVariants = {
@@ -163,10 +180,20 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
163
180
  animate: { opacity: 1, y: 0 },
164
181
  exit: { opacity: 0, y: -20 },
165
182
  };
166
-
183
+
184
+ // GitHub status helpers
185
+ const githubConnectedAccount = useMemo(() => {
186
+ return (connected || []).find(c => (c?.name || '').toLowerCase() === 'github');
187
+ }, [connected]);
188
+ const isGithubConnected = !!githubConnectedAccount;
189
+ const isGithubAppInstalled = useMemo(() => {
190
+ const setupAction = (githubConnectedAccount as any)?.app_installation_info?.setupAction;
191
+ return setupAction === 'install';
192
+ }, [githubConnectedAccount]);
193
+
167
194
  return (
168
195
  <>
169
- {selectedProvider ? (
196
+ {selectedProvider && selectedProvider.id !== 'githubapp' ? (
170
197
  <AnimatePresence>
171
198
  <motion.div
172
199
  key="connect-card"
@@ -255,6 +282,165 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
255
282
  </div>
256
283
  </motion.div>
257
284
  </AnimatePresence>
285
+ ) : selectedProvider && selectedProvider.id === 'githubapp' ? (
286
+ <AnimatePresence>
287
+ {(!showGithubManage && initialProviderId === 'githubapp') ? (
288
+ <motion.div
289
+ key="github-card"
290
+ className="rounded-xl border max-w-xl w-full"
291
+ style={{
292
+ backgroundColor: 'var(--content-card-background)',
293
+ borderColor: 'var(--icon-button-secondary)',
294
+ }}
295
+ variants={cardVariants}
296
+ initial="initial"
297
+ animate="animate"
298
+ exit="exit"
299
+ transition={{ duration: 0.3 }}
300
+ >
301
+ <div className="p-6 flex flex-col items-center">
302
+ <div className="w-full flex items-center gap-3 mb-2 justify-center">
303
+ <ProviderIcon name="github" className="w-8 h-8 inline-block" />
304
+ <span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
305
+ </div>
306
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
307
+ You’ve successfully linked your GitHub account!
308
+ <br />
309
+ To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you’d like to highlight private work or share additional contributions for verification.
310
+ <br /><br />
311
+ <span className="text-[var(--text-main)] font-medium">
312
+ Would you like to connect your private repositories?
313
+ </span>
314
+ </p>
315
+ <div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
316
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
317
+ <Button
318
+ className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
319
+ variant="destructive"
320
+ onClick={() => {
321
+ handleConnectBack();
322
+ }}
323
+ >
324
+ No, don&#39;t connect
325
+ </Button>
326
+ </motion.div>
327
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
328
+ <Button
329
+ className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
330
+ onClick={() => {
331
+ const opaqueState = window.crypto?.randomUUID?.() || Math.random().toString(36).substring(2);
332
+ const stateObj = { opaqueState, userId, companyId };
333
+ const stateString = encodeURIComponent(JSON.stringify(stateObj));
334
+ const redirectUrl = `https://github.com/apps/${githubAppSlugId}/installations/new?state=${stateString}`;
335
+ window.location.href = redirectUrl;
336
+ }}
337
+ >
338
+ <span className="flex items-center justify-center">
339
+ <ExternalLink className="w-4 h-4 mr-2" />
340
+ Yes, connect my private repos
341
+ </span>
342
+ </Button>
343
+ </motion.div>
344
+ </div>
345
+ </div>
346
+ </motion.div>
347
+ ) : (
348
+ <motion.div
349
+ key="github-manage"
350
+ className="rounded-xl border max-w-xl w-full"
351
+ style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
352
+ variants={cardVariants}
353
+ initial="initial"
354
+ animate="animate"
355
+ exit="exit"
356
+ >
357
+ <div className="sm:p-6 p-4">
358
+ <button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
359
+ <ArrowLeft className="w-4 h-4" />
360
+ Back
361
+ </button>
362
+ <div className="text-center">
363
+ <div className="flex justify-center mb-4">
364
+ <ProviderIcon name="github" className="w-10 h-10" />
365
+ </div>
366
+ <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
367
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
368
+ Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
369
+ </p>
370
+ </div>
371
+
372
+ <div className="mt-6 space-y-4">
373
+ <div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
374
+ <div className="flex items-center justify-between">
375
+ <div>
376
+ <div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub OAuth</div>
377
+ <div className="sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>{isGithubConnected ? 'Connected' : 'Not connected'}</div>
378
+ </div>
379
+ <div className="flex items-center gap-2">
380
+ {isGithubConnected ? (
381
+ <Button
382
+ onClick={() => onDisconnect('github')}
383
+ className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
384
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
385
+ variant="destructive"
386
+ disabled={isDisconnecting === 'github'}
387
+ >
388
+ {isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
389
+ <span>Disconnect</span>
390
+ </Button>
391
+ ) : (
392
+ <Button
393
+ onClick={() => onOAuth('github')}
394
+ className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
395
+ >
396
+ <ExternalLink className="w-4 h-4 mr-2" />
397
+ Connect
398
+ </Button>
399
+ )}
400
+ </div>
401
+ </div>
402
+ </div>
403
+
404
+ <div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
405
+ <div className="flex items-center justify-between">
406
+ <div>
407
+ <div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub App (Private Repos)</div>
408
+ <div className="sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>{isGithubAppInstalled ? 'Installed' : 'Not installed'}</div>
409
+ </div>
410
+ <div className="flex items-center gap-2">
411
+ {isGithubAppInstalled ? (
412
+ <Button
413
+ onClick={() => { window.location.href = 'https://github.com/settings/installations'; }}
414
+ className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
415
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
416
+ variant="destructive"
417
+ >
418
+ <Unlink className="size-3 sm:size-4" />
419
+ <span>Uninstall</span>
420
+ </Button>
421
+ ) : (
422
+ <Button
423
+ onClick={() => {
424
+ const opaqueState = window.crypto?.randomUUID?.() || Math.random().toString(36).substring(2);
425
+ const stateObj = { opaqueState, userId };
426
+ const stateString = encodeURIComponent(JSON.stringify(stateObj));
427
+ const redirectUrl = `https://github.com/apps/${githubAppSlugId}/installations/new?state=${stateString}`;
428
+ window.location.href = redirectUrl;
429
+ }}
430
+ className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
431
+ >
432
+ <ExternalLink className="w-4 h-4 mr-2" />
433
+ Install
434
+ </Button>
435
+ )}
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </div>
441
+ </motion.div>
442
+ )}
443
+ </AnimatePresence>
258
444
  ) : (
259
445
  <Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
260
446
  <AnimatePresence mode="wait">
@@ -279,7 +465,7 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
279
465
  const isConnected = connectedIds.has(providerId.toLowerCase());
280
466
  const needsReconnect = reconnectIds.has(providerId.toLowerCase());
281
467
  const isOauth = provider.connectionType === 'oauth'
282
- const connectedUrl = connected.find(c => c.id.toLowerCase() === providerId.toLowerCase())?.url;
468
+ const connectedUrl = connected.find(c => c.name.toLowerCase() === providerId.toLowerCase())?.url;
283
469
  const betaFlag = () => (
284
470
  <span
285
471
  className="ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200"
@@ -324,7 +510,49 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
324
510
  {provider.beta ? betaFlag() : null}
325
511
  </div>
326
512
 
327
- {isConnected && isDisconnecting !== providerId ? (
513
+ {providerId.toLowerCase() === 'github' ? (
514
+ <div className="flex items-center gap-2">
515
+ {(!isConnected && !isGithubAppInstalled) && (
516
+ <Button
517
+ onClick={() => setSelectedProviderIdAndCallback(providerId)}
518
+ className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
519
+ >
520
+ <Link2 className="size-3 sm:size-4" />
521
+ <span className="sm:text-base text-sm">Connect</span>
522
+ </Button>
523
+ )}
524
+ {(isConnected && !isGithubAppInstalled) && (
525
+ <Button
526
+ onClick={() => { setShowGithubManage(true); setSelectedProviderIdAndCallback('githubapp'); }}
527
+ className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
528
+ >
529
+ <Settings className="size-3 sm:size-4" />
530
+ <span className="sm:text-base text-sm">Manage</span>
531
+ </Button>
532
+ )}
533
+ {(isConnected && isGithubAppInstalled) && (
534
+ <div
535
+ className="relative flex items-center group"
536
+ onClick={() => { setShowGithubManage(true); setSelectedProviderIdAndCallback('githubapp'); }}
537
+ style={{ cursor: 'pointer' }}
538
+ >
539
+ <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
540
+ <CheckCircle className="size-3 sm:size-4" />
541
+ <span className="text-sm font-medium">Connected</span>
542
+ </div>
543
+ <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
544
+ <div className="flex items-center gap-1.5 text-red-600 text-sm hover:underline">
545
+ <Unlink className="size-3 sm:size-4" />
546
+ <span>Disconnect</span>
547
+ </div>
548
+ </div>
549
+ </div>
550
+ )}
551
+ {isDisconnecting === providerId && (
552
+ <Spinner />
553
+ )}
554
+ </div>
555
+ ) : isConnected && isDisconnecting !== providerId ? (
328
556
  <div className="relative flex items-center">
329
557
  <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
330
558
  <CheckCircle className="size-3 sm:size-4" />
@@ -333,14 +561,13 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
333
561
  <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
334
562
  <button
335
563
  onClick={() => onDisconnect(providerId)}
336
- className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
564
+ className="inline-flex items-center gap-1.5 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
337
565
  >
338
566
  <Unlink className="size-3 sm:size-4" />
339
567
  <span>Disconnect</span>
340
568
  </button>
341
569
  </div>
342
570
  </div>
343
-
344
571
  ) : isDisconnecting === providerId ? (
345
572
  <div className="relative flex items-center">
346
573
  <Spinner />
@@ -415,3 +642,4 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
415
642
  export default ConnectAccounts;
416
643
 
417
644
 
645
+
@@ -1,7 +1,4 @@
1
- export type ConnectedAccountLite = {
2
- id: string;
3
- url?: string | null;
4
- };
1
+ import { ConnectedAccount } from '../types';
5
2
 
6
3
  export type RedirectUriOverrides = Partial<Record<'github' | 'gitlab' | 'stackoverflow', string>>;
7
4
 
@@ -18,7 +15,7 @@ export interface ConnectAccountsProps {
18
15
  copy?: string;
19
16
  beta?: boolean;
20
17
  }>;
21
- connected: ConnectedAccountLite[];
18
+ connected: ConnectedAccount[];
22
19
  idToken: string;
23
20
  apiGatewayUrl?: string;
24
21
  companyId?: string;
@@ -39,6 +36,9 @@ export interface ConnectAccountsProps {
39
36
  isDisconnecting?: string | null;
40
37
  requiredProviders?: string[];
41
38
  companyName?: string;
39
+ initialProviderId?: string;
40
+ githubAppSlugId: string;
41
+ userId: string;
42
42
  }
43
43
 
44
44
 
package/src/types.ts CHANGED
@@ -22,18 +22,28 @@ export interface PublicBadgeData {
22
22
  assessmentResult: AssessmentResult;
23
23
  updatedAt: string;
24
24
  badgeImageUrl?: string;
25
- connectedAccounts?: {
26
- name: string;
27
- url?: string;
28
- handle?: string;
29
- observedAt?: string;
30
- }[];
25
+ connectedAccounts?: ConnectedAccount[];
31
26
  optOutScreening?: boolean;
32
27
  isPublic: boolean;
33
28
  companyName?: string;
34
29
  providersToReconnect?: string[];
35
30
  }
36
31
 
32
+ export interface AppInstallationInfo {
33
+ installationId: string;
34
+ setupAction: string;
35
+ stateOpaque: string;
36
+ verifiedAt: string;
37
+ }
38
+
39
+ export interface ConnectedAccount {
40
+ name: string;
41
+ url?: string;
42
+ handle?: string;
43
+ observedAt?: string;
44
+ app_installation_info?: AppInstallationInfo;
45
+ }
46
+
37
47
  export type User = {
38
48
  userId: string;
39
49
  name: string;
@@ -47,7 +57,7 @@ export type User = {
47
57
  latestBadgeId?: string;
48
58
  latestBadgeImageUrl?: string;
49
59
  assessments: PublicBadgeData[];
50
- connectedAccounts?: { name: string; url: string | null; handle: string | null; observedAt?: string }[];
60
+ connectedAccounts?: ConnectedAccount[];
51
61
  userPendingRetryBadgeId?: string;
52
62
  createdAt?: string;
53
63
  };