kyd-shared-badge 0.3.70 → 0.3.71
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 +2 -1
- package/src/connect/ConnectAccounts.tsx +235 -119
- package/src/connect/oauth.ts +13 -8
- package/src/connect/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.71",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
24
|
"ai": "5.0.47",
|
|
25
|
+
"framer-motion": "^12.23.24",
|
|
25
26
|
"i18n-iso-countries": "^7.14.0",
|
|
26
27
|
"lucide-react": "^0.545.0",
|
|
27
28
|
"next-auth": "^4.24.11",
|
|
@@ -3,8 +3,10 @@ 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 } from 'lucide-react';
|
|
7
|
-
import {
|
|
6
|
+
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink } from 'lucide-react';
|
|
7
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
|
+
import { Button, Input, Spinner } from '../ui';
|
|
9
|
+
import Link from 'next/link';
|
|
8
10
|
|
|
9
11
|
function byPriority(a: string, b: string) {
|
|
10
12
|
const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2);
|
|
@@ -28,6 +30,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
28
30
|
onDisconnected,
|
|
29
31
|
onError,
|
|
30
32
|
className,
|
|
33
|
+
oauthClientIds,
|
|
34
|
+
providerPickCallback,
|
|
31
35
|
} = props;
|
|
32
36
|
|
|
33
37
|
const router = useRouter();
|
|
@@ -89,6 +93,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
|
|
96
|
+
const setSelectedProviderIdAndCallback = (providerId: string | null) => {
|
|
97
|
+
setSelectedProviderId(providerId);
|
|
98
|
+
providerPickCallback(providerId);
|
|
99
|
+
};
|
|
100
|
+
|
|
92
101
|
const onSubmitLink = async (providerId: string) => {
|
|
93
102
|
const provider = providers.find(p => p.id.toLowerCase() === providerId.toLowerCase());
|
|
94
103
|
if (!provider || !provider.endpoint) return;
|
|
@@ -106,7 +115,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
106
115
|
});
|
|
107
116
|
const data = await res.json().catch(() => ({}));
|
|
108
117
|
if (!res.ok) throw new Error(data?.error || `Failed to sync ${provider.name} profile.`);
|
|
109
|
-
|
|
118
|
+
setSelectedProviderIdAndCallback(null);
|
|
119
|
+
|
|
110
120
|
setLinkUrl('');
|
|
111
121
|
if (onConnected) onConnected(providerId);
|
|
112
122
|
} catch (e) {
|
|
@@ -124,136 +134,242 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
124
134
|
// Lazy import builder to avoid circular deps
|
|
125
135
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
126
136
|
const { buildOAuthUrl } = require('./oauth');
|
|
127
|
-
const url = buildOAuthUrl({ providerId: id, redirectUri, state });
|
|
137
|
+
const url = buildOAuthUrl({ providerId: id, redirectUri, state, clientIds: oauthClientIds });
|
|
128
138
|
if (url) router.push(url);
|
|
129
139
|
};
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
141
|
+
const selectedProvider = useMemo(
|
|
142
|
+
() => (selectedProviderId ? providers.find(p => p.id.toLowerCase() === selectedProviderId.toLowerCase()) : null),
|
|
143
|
+
[selectedProviderId, providers]
|
|
144
|
+
);
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
const handleConnectBack = () => {
|
|
147
|
+
setSelectedProviderIdAndCallback(null);
|
|
148
|
+
setLinkUrl('');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const fadeIn = {
|
|
152
|
+
hidden: { opacity: 0, y: 6 },
|
|
153
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.15 } },
|
|
154
|
+
exit: { opacity: 0, y: 6, transition: { duration: 0.1 } },
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
{selectedProvider ? (
|
|
160
|
+
<AnimatePresence>
|
|
161
|
+
<motion.div
|
|
162
|
+
key="connect-card"
|
|
163
|
+
className="rounded-xl border max-w-xl w-full"
|
|
164
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
165
|
+
initial="hidden" animate="visible" exit="exit" variants={fadeIn}
|
|
166
|
+
>
|
|
167
|
+
<div className="sm:p-6 p-4">
|
|
168
|
+
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
169
|
+
<ArrowLeft className="w-4 h-4" />
|
|
170
|
+
Back
|
|
171
|
+
</button>
|
|
172
|
+
<div className="text-center">
|
|
173
|
+
<div className="flex justify-center mb-4">
|
|
174
|
+
<ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
|
|
175
|
+
</div>
|
|
176
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
177
|
+
{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
|
|
178
|
+
? `Use Public ${selectedProvider.name} Profile`
|
|
179
|
+
: `Connect ${selectedProvider.name}`}
|
|
180
|
+
</h3>
|
|
181
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
182
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
183
|
+
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
184
|
+
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
185
|
+
</p>
|
|
156
186
|
</div>
|
|
157
187
|
|
|
158
|
-
{
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
|
|
189
|
+
<motion.form
|
|
190
|
+
onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
|
|
191
|
+
className="mt-6 space-y-4"
|
|
192
|
+
initial="hidden" animate="visible" variants={fadeIn}
|
|
193
|
+
>
|
|
194
|
+
{selectedProvider.id === 'linkedin' && (
|
|
195
|
+
<p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
|
|
196
|
+
<Link
|
|
197
|
+
href="https://www.linkedin.com/public-profile/settings"
|
|
198
|
+
target="_blank"
|
|
199
|
+
rel="noopener noreferrer"
|
|
200
|
+
className="underline"
|
|
201
|
+
style={{ color: 'var(--icon-accent)' }}
|
|
202
|
+
>
|
|
203
|
+
LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
|
|
204
|
+
</Link>
|
|
205
|
+
. This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
|
|
206
|
+
</p>
|
|
207
|
+
)}
|
|
208
|
+
<div className="relative">
|
|
209
|
+
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
210
|
+
<Input
|
|
211
|
+
type="url"
|
|
212
|
+
value={linkUrl}
|
|
213
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
214
|
+
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
215
|
+
required
|
|
216
|
+
className="w-full border bg-transparent p-2 pl-9"
|
|
217
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
218
|
+
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
219
|
+
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
220
|
+
/>
|
|
164
221
|
</div>
|
|
165
|
-
<div
|
|
166
|
-
<Button
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
222
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
223
|
+
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
224
|
+
{isSubmitting ? (
|
|
225
|
+
<div className="flex items-center justify-center">
|
|
226
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
227
|
+
Connecting...
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
'Connect'
|
|
231
|
+
)}
|
|
173
232
|
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</
|
|
233
|
+
</motion.div>
|
|
234
|
+
</motion.form>
|
|
176
235
|
) : (
|
|
177
|
-
<div className="
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
onChange={(e) => setLinkUrl(e.target.value)}
|
|
185
|
-
placeholder={provider.placeholder || 'https://example.com/your-profile'}
|
|
186
|
-
required
|
|
187
|
-
className="w-72 border bg-transparent p-2 text-sm rounded"
|
|
188
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
189
|
-
onPaste={providerId === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
190
|
-
onBlur={providerId === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
193
|
-
<Button type="submit" disabled={isSubmitting} className="px-3 py-2 rounded bg-[var(--icon-accent)] text-white">
|
|
194
|
-
{isSubmitting ? 'Connecting…' : 'Connect'}
|
|
195
|
-
</Button>
|
|
196
|
-
</form>
|
|
197
|
-
) : (
|
|
198
|
-
<>
|
|
199
|
-
<Button
|
|
200
|
-
onClick={() => (isOauth ? onOAuth(providerId) : setSelectedProviderId(providerId))}
|
|
201
|
-
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"
|
|
202
|
-
>
|
|
203
|
-
{/* OAuth/URL leading icon */}
|
|
204
|
-
{isOauth ? (
|
|
205
|
-
<Link2 className="size-3 sm:size-4" />
|
|
206
|
-
) : (
|
|
207
|
-
<LinkIcon className="size-3 sm:size-4" />
|
|
208
|
-
)}
|
|
209
|
-
<span className="sm:text-base text-sm">Connect</span>
|
|
210
|
-
</Button>
|
|
211
|
-
{needsReconnect && (
|
|
212
|
-
<Button
|
|
213
|
-
onClick={() => onDisconnect(providerId)}
|
|
214
|
-
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
215
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
216
|
-
>
|
|
217
|
-
{/* Unlink icon (inline SVG) */}
|
|
218
|
-
<Unlink className="size-3 sm:size-4" />
|
|
219
|
-
<span>Remove</span>
|
|
220
|
-
</Button>
|
|
221
|
-
)}
|
|
222
|
-
</>
|
|
223
|
-
)}
|
|
236
|
+
<div className="mt-6">
|
|
237
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
238
|
+
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
239
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
240
|
+
Connect with {selectedProvider.name}
|
|
241
|
+
</Button>
|
|
242
|
+
</motion.div>
|
|
224
243
|
</div>
|
|
225
244
|
)}
|
|
226
245
|
</div>
|
|
227
|
-
|
|
228
|
-
|
|
246
|
+
</motion.div>
|
|
247
|
+
</AnimatePresence>
|
|
248
|
+
) : (
|
|
249
|
+
<motion.div className="rounded-xl border max-w-lg" style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}} initial="hidden" animate="visible" variants={fadeIn}>
|
|
250
|
+
<div className="sm:p-6 p-4">
|
|
251
|
+
<h2 className="sm:text-xl text-base font-semibold" style={{ color: 'var(--text-main)'}}>Connect accounts</h2>
|
|
252
|
+
<p className="mt-2 sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>
|
|
253
|
+
Each connected account adds verified signals to your report. Connect more accounts to strengthen your score—then disconnect anytime.
|
|
254
|
+
</p>
|
|
255
|
+
</div>
|
|
256
|
+
<motion.div className="sm:px-6 px-4 sm:pb-6 pb-4 space-y-2" initial="hidden" animate="visible" variants={fadeIn}>
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
{(() => {
|
|
259
|
+
const oauthList = list.filter(p => p.connectionType === 'oauth');
|
|
260
|
+
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
229
261
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
const Row = (provider: typeof list[number]) => {
|
|
263
|
+
const providerId = provider.id;
|
|
264
|
+
const isConnected = connectedIds.has(providerId.toLowerCase());
|
|
265
|
+
const needsReconnect = reconnectIds.has(providerId.toLowerCase());
|
|
266
|
+
const isOauth = provider.connectionType === 'oauth'
|
|
267
|
+
const connectedUrl = connected.find(c => c.id.toLowerCase() === providerId.toLowerCase())?.url;
|
|
268
|
+
const betaFlag = () => (
|
|
269
|
+
<span
|
|
270
|
+
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"
|
|
271
|
+
style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
|
|
272
|
+
>
|
|
273
|
+
Beta
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
242
276
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
277
|
+
return (
|
|
278
|
+
<div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10 transform transition-transform hover:-translate-y-px">
|
|
279
|
+
<div className="flex items-center gap-3">
|
|
280
|
+
|
|
281
|
+
{isConnected && connectedUrl ? (
|
|
282
|
+
<Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
|
|
283
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
284
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
285
|
+
</Link>
|
|
286
|
+
) : (
|
|
287
|
+
<div className="flex items-center gap-3">
|
|
288
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
289
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
{provider.beta ? betaFlag() : null}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{isConnected ? (
|
|
296
|
+
<div className="relative flex items-center">
|
|
297
|
+
<div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
|
|
298
|
+
<CheckCircle className="size-3 sm:size-4" />
|
|
299
|
+
<span className="text-sm font-medium">Connected</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => onDisconnect(providerId)}
|
|
304
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
|
|
305
|
+
>
|
|
306
|
+
<Unlink className="size-3 sm:size-4" />
|
|
307
|
+
<span>Disconnect</span>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<>
|
|
314
|
+
<Button
|
|
315
|
+
onClick={() => setSelectedProviderIdAndCallback(providerId)}
|
|
316
|
+
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"
|
|
317
|
+
>
|
|
318
|
+
{isOauth ? (
|
|
319
|
+
<Link2 className="size-3 sm:size-4" />
|
|
320
|
+
) : (
|
|
321
|
+
<LinkIcon className="size-3 sm:size-4" />
|
|
322
|
+
)}
|
|
323
|
+
<span className="sm:text-base text-sm">Connect</span>
|
|
324
|
+
</Button>
|
|
325
|
+
{needsReconnect && (
|
|
326
|
+
<Button
|
|
327
|
+
onClick={() => onDisconnect(providerId)}
|
|
328
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
329
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
330
|
+
>
|
|
331
|
+
<Unlink className="size-3 sm:size-4" />
|
|
332
|
+
<span>Remove</span>
|
|
333
|
+
</Button>
|
|
334
|
+
)}
|
|
335
|
+
</>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<>
|
|
344
|
+
{oauthList.length ? (
|
|
345
|
+
<>
|
|
346
|
+
<div className="flex items-center my-2">
|
|
347
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
348
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
|
|
349
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
350
|
+
</div>
|
|
351
|
+
{oauthList.map(p => Row(p))}
|
|
352
|
+
</>
|
|
353
|
+
) : null}
|
|
354
|
+
|
|
355
|
+
{urlList.length ? (
|
|
356
|
+
<>
|
|
357
|
+
<div className="flex items-center my-2">
|
|
358
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
359
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
|
|
360
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
361
|
+
</div>
|
|
362
|
+
{urlList.map(p => Row(p))}
|
|
363
|
+
</>
|
|
364
|
+
) : null}
|
|
365
|
+
</>
|
|
366
|
+
);
|
|
367
|
+
})()}
|
|
368
|
+
</div>
|
|
369
|
+
</motion.div>
|
|
370
|
+
</motion.div>
|
|
371
|
+
)}
|
|
372
|
+
</>
|
|
257
373
|
);
|
|
258
374
|
}
|
|
259
375
|
|
package/src/connect/oauth.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
type KnownProvider = 'github' | 'gitlab' | 'stackoverflow';
|
|
2
2
|
|
|
3
3
|
const env = (key: string): string | undefined => {
|
|
4
|
-
//
|
|
5
|
-
try {
|
|
4
|
+
// Use globalThis to avoid hard dependency on Node 'process' types/environ
|
|
5
|
+
try {
|
|
6
|
+
const g = globalThis as unknown as { process?: { env?: Record<string, string | undefined> } };
|
|
7
|
+
return g?.process?.env?.[key];
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
6
11
|
};
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
github: env('NEXT_PUBLIC_GITHUB_CLIENT_ID'),
|
|
10
|
-
gitlab: env('NEXT_PUBLIC_GITLAB_CLIENT_ID'),
|
|
11
|
-
stackoverflow: env('NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'),
|
|
12
|
-
};
|
|
13
|
+
export type OAuthClientIds = Partial<Record<KnownProvider, string>>;
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SCOPES: Record<KnownProvider, string> = {
|
|
15
16
|
github: 'read:user,repo',
|
|
@@ -22,9 +23,13 @@ export function buildOAuthUrl(params: {
|
|
|
22
23
|
redirectUri: string;
|
|
23
24
|
state?: string;
|
|
24
25
|
scopeOverride?: string;
|
|
26
|
+
clientIds?: OAuthClientIds;
|
|
25
27
|
}): string {
|
|
26
28
|
const id = (params.providerId || '').toLowerCase() as KnownProvider;
|
|
27
|
-
const clientId =
|
|
29
|
+
const clientId = (params.clientIds && params.clientIds[id]) || env(
|
|
30
|
+
id === 'github' ? 'NEXT_PUBLIC_GITHUB_CLIENT_ID' : id === 'gitlab' ? 'NEXT_PUBLIC_GITLAB_CLIENT_ID' : 'NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'
|
|
31
|
+
);
|
|
32
|
+
// clientId should be inlined at build-time for NEXT_PUBLIC_*
|
|
28
33
|
const scope = params.scopeOverride ?? DEFAULT_SCOPES[id as KnownProvider] ?? '';
|
|
29
34
|
const redirect = encodeURIComponent(params.redirectUri);
|
|
30
35
|
const state = params.state ? `&state=${encodeURIComponent(params.state)}` : '';
|
package/src/connect/types.ts
CHANGED
|
@@ -27,10 +27,12 @@ export interface ConnectAccountsProps {
|
|
|
27
27
|
buildOAuthState?: () => Record<string, string | number | boolean> | string | undefined;
|
|
28
28
|
stateFormat?: StateFormat;
|
|
29
29
|
redirectUriOverrides?: RedirectUriOverrides;
|
|
30
|
+
oauthClientIds?: Partial<Record<'github' | 'gitlab' | 'stackoverflow', string>>;
|
|
30
31
|
onConnected?: (providerId: string) => void;
|
|
31
32
|
onDisconnected?: (providerId: string) => void;
|
|
32
33
|
onError?: (message: string) => void;
|
|
33
34
|
className?: string;
|
|
35
|
+
providerPickCallback: (providerId: string | null) => void;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
|