Skip to content

Commit 6cbb936

Browse files
committed
poc: observable signin
1 parent 607d333 commit 6cbb936

File tree

11 files changed

+617
-102
lines changed

11 files changed

+617
-102
lines changed

packages/clerk-js/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
"dequal": "2.0.3",
7878
"qrcode.react": "4.2.0",
7979
"regenerator-runtime": "0.14.1",
80-
"swr": "2.3.3"
80+
"swr": "2.3.3",
81+
"zustand": "5.0.5"
8182
},
8283
"devDependencies": {
8384
"@rsdoctor/rspack-plugin": "^0.4.13",

packages/clerk-js/sandbox/app.ts

Lines changed: 244 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { SignInResource } from '@clerk/types';
2+
13
import * as l from '../../localizations';
24
import type { Clerk as ClerkType } from '../';
35

@@ -34,6 +36,7 @@ const AVAILABLE_COMPONENTS = [
3436
'waitlist',
3537
'pricingTable',
3638
'oauthConsent',
39+
'signInObservable',
3740
] as const;
3841

3942
const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox';
@@ -93,6 +96,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component
9396
waitlist: buildComponentControls('waitlist'),
9497
pricingTable: buildComponentControls('pricingTable'),
9598
oauthConsent: buildComponentControls('oauthConsent'),
99+
signInObservable: buildComponentControls('signInObservable'),
96100
};
97101

98102
declare global {
@@ -257,6 +261,229 @@ function otherOptions() {
257261
return { updateOtherOptions };
258262
}
259263

264+
function mountSignInObservable(element: HTMLDivElement) {
265+
assertClerkIsLoaded(Clerk);
266+
267+
// Create container for status display
268+
const statusContainer = document.createElement('div');
269+
statusContainer.className = 'p-4 border border-gray-200 rounded-md mb-4';
270+
element.appendChild(statusContainer);
271+
272+
// Create controls container
273+
const controlsContainer = document.createElement('div');
274+
controlsContainer.style.marginBottom = '1rem';
275+
controlsContainer.style.display = 'flex';
276+
controlsContainer.style.flexDirection = 'column';
277+
278+
// Create store state display
279+
const storeStateDisplay = document.createElement('div');
280+
storeStateDisplay.className = 'p-2 bg-gray-50 rounded text-sm font-mono';
281+
282+
// Append store state display to controlsContainer
283+
controlsContainer.appendChild(storeStateDisplay);
284+
285+
element.appendChild(controlsContainer);
286+
287+
// Create sign in form
288+
const form = document.createElement('form');
289+
form.className = 'space-y-4';
290+
291+
const emailInput = document.createElement('input');
292+
emailInput.type = 'email';
293+
emailInput.placeholder = 'Email';
294+
emailInput.className = 'w-full p-2 border rounded';
295+
296+
const passwordInput = document.createElement('input');
297+
passwordInput.type = 'password';
298+
passwordInput.placeholder = 'Password';
299+
passwordInput.className = 'w-full p-2 border rounded';
300+
301+
const submitButton = document.createElement('button');
302+
submitButton.type = 'submit';
303+
submitButton.textContent = 'Sign In';
304+
submitButton.className = 'w-full p-2 bg-blue-500 text-white rounded';
305+
306+
form.appendChild(emailInput);
307+
form.appendChild(passwordInput);
308+
form.appendChild(submitButton);
309+
element.appendChild(form);
310+
311+
let signIn: SignInResource;
312+
313+
let isInitialized = false;
314+
315+
// Create updateStatus function in the outer scope
316+
const updateStatus = () => {
317+
if (!signIn) {
318+
console.error('SignIn object is not initialized');
319+
return;
320+
}
321+
const fetchStatus = signIn.fetchStatus;
322+
const error = signIn.signInError.global;
323+
const status = signIn.status;
324+
325+
// Update status container with animation
326+
statusContainer.innerHTML = `
327+
<div class="space-y-2 transition-all duration-300">
328+
<div class="flex items-center gap-2">
329+
<strong>Fetch Status:</strong>
330+
<span class="px-2 py-0.5 rounded text-sm ${
331+
fetchStatus === 'fetching'
332+
? 'bg-blue-100 text-blue-700'
333+
: fetchStatus === 'error'
334+
? 'bg-red-100 text-red-700'
335+
: 'bg-green-100 text-green-700'
336+
}">${fetchStatus}</span>
337+
</div>
338+
<div class="flex items-center gap-2">
339+
<strong>Sign In Status:</strong>
340+
<span class="px-2 py-0.5 rounded text-sm ${
341+
status === 'needs_first_factor'
342+
? 'bg-yellow-100 text-yellow-700'
343+
: status === 'complete'
344+
? 'bg-green-100 text-green-700'
345+
: 'bg-gray-100 text-gray-700'
346+
}">${status || 'null'}</span>
347+
</div>
348+
${error ? `<div class="text-red-500"><strong>Error:</strong> ${error}</div>` : ''}
349+
</div>
350+
`;
351+
352+
// Update store state display
353+
storeStateDisplay.innerHTML = `
354+
<div class="space-y-1">
355+
<div>Store State:</div>
356+
<pre class="whitespace-pre-wrap">${JSON.stringify(
357+
{
358+
fetchStatus,
359+
status,
360+
error: error || null,
361+
},
362+
null,
363+
2,
364+
)}</pre>
365+
</div>
366+
`;
367+
};
368+
369+
// Initialize SignIn instance
370+
const initializeSignIn = async () => {
371+
try {
372+
// Show loading state
373+
statusContainer.innerHTML = `
374+
<div class="text-blue-500">
375+
<strong>Status:</strong> Initializing...
376+
</div>
377+
`;
378+
379+
// Wait for Clerk to be loaded and client to be ready
380+
const waitForClerk = async () => {
381+
if (!Clerk.loaded) {
382+
await new Promise<void>(resolve => {
383+
const checkLoaded = () => {
384+
if (Clerk.loaded) {
385+
resolve();
386+
} else {
387+
setTimeout(checkLoaded, 100);
388+
}
389+
};
390+
checkLoaded();
391+
});
392+
}
393+
394+
// Wait for client to be ready
395+
if (!Clerk.client) {
396+
await new Promise<void>(resolve => {
397+
const checkClient = () => {
398+
if (Clerk.client) {
399+
resolve();
400+
} else {
401+
setTimeout(checkClient, 100);
402+
}
403+
};
404+
checkClient();
405+
});
406+
}
407+
};
408+
409+
await waitForClerk();
410+
411+
// Initial update
412+
updateStatus();
413+
414+
// Update status to show initialization complete
415+
statusContainer.innerHTML = `
416+
<div class="text-green-500">
417+
<strong>Status:</strong> Ready to sign in
418+
</div>
419+
`;
420+
} catch (error) {
421+
console.error('Failed to initialize:', error);
422+
statusContainer.innerHTML = `
423+
<div class="text-red-500">
424+
<strong>Error:</strong> ${error instanceof Error ? error.message : 'Failed to initialize'}
425+
</div>
426+
`;
427+
isInitialized = false;
428+
}
429+
};
430+
431+
// Handle form submission
432+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
433+
form.addEventListener('submit', async e => {
434+
e.preventDefault();
435+
436+
try {
437+
if (!isInitialized || !Clerk.client) {
438+
throw new Error('System not initialized. Please wait...');
439+
}
440+
441+
// Show loading state
442+
statusContainer.innerHTML = `
443+
<div class="text-blue-500">
444+
<strong>Status:</strong> Processing sign in...
445+
</div>
446+
`;
447+
448+
// Create SignIn instance with the provided email
449+
signIn = await Clerk.client.signIn.create({
450+
identifier: emailInput.value,
451+
strategy: 'email_code',
452+
});
453+
454+
if (!signIn) {
455+
throw new Error('Failed to create SignIn instance');
456+
}
457+
458+
// Initial update using getters
459+
updateStatus();
460+
461+
await signIn.prepareFirstFactor({
462+
strategy: 'email_code',
463+
emailAddressId: emailInput.value,
464+
});
465+
466+
await signIn.attemptFirstFactor({
467+
strategy: 'email_code',
468+
code: passwordInput.value,
469+
});
470+
471+
// Update status after sign-in attempt
472+
updateStatus();
473+
} catch (error) {
474+
console.error('Sign in error:', error);
475+
statusContainer.innerHTML = `
476+
<div class="text-red-500">
477+
<strong>Error:</strong> ${error instanceof Error ? error.message : 'An error occurred'}
478+
</div>
479+
`;
480+
}
481+
});
482+
483+
// Initialize on mount
484+
void initializeSignIn();
485+
}
486+
260487
void (async () => {
261488
assertClerkIsLoaded(Clerk);
262489
fillLocalizationSelect();
@@ -333,6 +560,22 @@ void (async () => {
333560
'/open-sign-up': () => {
334561
mountOpenSignUpButton(app, componentControls.signUp.getProps() ?? {});
335562
},
563+
'/sign-in-observable': async () => {
564+
// Wait for Clerk to be fully loaded before mounting the component
565+
if (!Clerk.loaded) {
566+
await new Promise<void>(resolve => {
567+
const checkLoaded = () => {
568+
if (Clerk.loaded) {
569+
resolve();
570+
} else {
571+
setTimeout(checkLoaded, 100);
572+
}
573+
};
574+
checkLoaded();
575+
});
576+
}
577+
mountSignInObservable(app);
578+
},
336579
};
337580

338581
const route = window.location.pathname as keyof typeof routes;
@@ -344,7 +587,7 @@ void (async () => {
344587
signInUrl: '/sign-in',
345588
signUpUrl: '/sign-up',
346589
});
347-
renderCurrentRoute();
590+
await renderCurrentRoute();
348591
updateVariables();
349592
updateOtherOptions();
350593
} else {

packages/clerk-js/sandbox/template.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@
188188
>Sign In</a
189189
>
190190
</li>
191+
<li class="relative">
192+
<a
193+
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
194+
href="/sign-in-observable"
195+
>Sign In Observable</a
196+
>
197+
</li>
191198
<li class="relative">
192199
<a
193200
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"

0 commit comments

Comments
 (0)