11import { Injectable } from '@angular/core' ;
22import { ElectronService } from '../electron/electron.service' ;
33import { events } from '@minsky/shared' ;
4- import { Clerk } from '@clerk/clerk-js' ;
4+ import type { Clerk } from '@clerk/clerk-js' ;
55import { AppConfig } from '@minsky/environment' ;
66
7+ declare global {
8+ interface Window {
9+ Clerk ?: Clerk ;
10+ __clerk_publishable_key ?: string ;
11+ }
12+ }
13+
714@Injectable ( {
815 providedIn : 'root' ,
916} )
@@ -16,15 +23,56 @@ export class ClerkService {
1623 async initialize ( ) : Promise < void > {
1724 if ( this . initialized ) return ;
1825
19- this . clerk = new Clerk ( AppConfig . clerkPublishableKey ) ;
20- // standardBrowser: true forces the UI component renderer to initialise even
21- // in non-standard browser contexts such as Electron (where environment
22- // detection may otherwise return false and leave #componentControls null,
23- // causing mountSignIn() to throw "Clerk was not loaded with Ui components").
24- await this . clerk . load ( { standardBrowser : true } ) ;
26+ // Load Clerk via its CDN browser bundle rather than the npm-imported module.
27+ // The npm dist files (clerk.js / clerk.mjs) do not bundle the ClerkUI
28+ // implementation, so clerk.mountSignIn() would always throw
29+ // "Clerk was not loaded with Ui components" when called on an instance
30+ // created with `new Clerk(key)` from the npm package.
31+ // The browser bundle served from Clerk's CDN lazily loads the UI chunks
32+ // (React-based pre-built components) from the same CDN origin, enabling
33+ // mountSignIn() to work correctly.
34+ await this . loadClerkBrowserBundle ( ) ;
35+ this . clerk = window . Clerk ! ;
36+ await this . clerk . load ( ) ;
2537 this . initialized = true ;
2638 }
2739
40+ /**
41+ * Dynamically injects Clerk's CDN browser bundle script into the document.
42+ * The CDN URL is derived from the publishable key's embedded frontendApi.
43+ * Returns a Promise that resolves once the script has loaded and
44+ * window.Clerk has been set by the bundle.
45+ */
46+ private loadClerkBrowserBundle ( ) : Promise < void > {
47+ return new Promise < void > ( ( resolve , reject ) => {
48+ if ( window . Clerk ) {
49+ resolve ( ) ;
50+ return ;
51+ }
52+
53+ const pk = AppConfig . clerkPublishableKey ;
54+ // Publishable key format: pk_<type>_<base64(frontendApi + '$')>
55+ const encoded = pk . split ( '_' ) [ 2 ] ?? '' ;
56+ const padded = encoded + '=' . repeat ( ( 4 - ( encoded . length % 4 ) ) % 4 ) ;
57+ let frontendApi : string ;
58+ try {
59+ frontendApi = atob ( padded ) . replace ( / \$ $ / , '' ) ;
60+ } catch {
61+ reject ( new Error ( 'Invalid Clerk publishable key' ) ) ;
62+ return ;
63+ }
64+
65+ const script = document . createElement ( 'script' ) ;
66+ script . src = `https://${ frontendApi } /npm/@clerk/clerk-js@latest/dist/clerk.browser.js` ;
67+ script . setAttribute ( 'data-clerk-publishable-key' , pk ) ;
68+ script . async = true ;
69+ script . onload = ( ) => resolve ( ) ;
70+ script . onerror = ( ) =>
71+ reject ( new Error ( 'Failed to load Clerk authentication service' ) ) ;
72+ document . head . appendChild ( script ) ;
73+ } ) ;
74+ }
75+
2876 async isSignedIn ( ) : Promise < boolean > {
2977 if ( ! this . clerk ) return false ;
3078 return ! ! this . clerk . user ;
@@ -35,7 +83,7 @@ export class ClerkService {
3583 return await this . clerk . session . getToken ( ) ;
3684 }
3785
38- mountSignIn ( element : HTMLElement ) : void {
86+ mountSignIn ( element : HTMLDivElement ) : void {
3987 if ( ! this . clerk ) throw new Error ( 'Clerk is not initialized.' ) ;
4088 this . clerk . mountSignIn ( element ) ;
4189 }
0 commit comments