@@ -10,16 +10,27 @@ import { authStore, generalSettingsStore } from "~/store";
1010import { identifyUser , trackEvent } from "./analytics" ;
1111import { commands } from "./tauri" ;
1212
13+ const paramsValidator = z . union ( [
14+ z . object ( {
15+ type : z . literal ( "api_key" ) ,
16+ api_key : z . string ( ) ,
17+ user_id : z . string ( ) ,
18+ } ) ,
19+ z . object ( {
20+ token : z . string ( ) ,
21+ user_id : z . string ( ) ,
22+ expires : z . coerce . number ( ) ,
23+ } ) ,
24+ ] ) ;
25+
26+ type AuthParams = z . infer < typeof paramsValidator > ;
27+
1328export function createSignInMutation ( ) {
1429 return createMutation ( ( ) => ( {
1530 mutationFn : async ( abort : AbortController ) => {
16- const platform = import . meta. env . DEV ? "web" : "desktop" ;
17-
18- let session ;
19-
20- if ( platform === "web" )
21- session = await createLocalServerSession ( abort . signal ) ;
22- else session = await createDeepLinkSession ( abort . signal ) ;
31+ const session = import . meta. env . DEV
32+ ? await createLocalServerSession ( abort . signal )
33+ : await createHybridDesktopSession ( abort . signal ) ;
2334
2435 await shell . open ( session . url . toString ( ) ) ;
2536
@@ -49,6 +60,59 @@ async function createSessionRequestUrl(
4960}
5061
5162async function createLocalServerSession ( signal : AbortSignal ) {
63+ const localCallback = await startLocalCallbackSession ( signal ) ;
64+
65+ return {
66+ url : await createSessionRequestUrl ( localCallback . port , "web" ) ,
67+ complete : async ( ) => {
68+ const result = await localCallback . complete ;
69+ await localCallback . dispose ( ) ;
70+
71+ if ( ! result ) return null ;
72+ if ( signal . aborted ) throw new Error ( "Sign in aborted" ) ;
73+
74+ return result ;
75+ } ,
76+ } ;
77+ }
78+
79+ async function createHybridDesktopSession ( signal : AbortSignal ) {
80+ const deepLink = await startDeepLinkSession ( signal ) ;
81+ const localCallback = await startLocalCallbackSession ( signal ) ;
82+
83+ return {
84+ url : await createSessionRequestUrl ( localCallback . port , "desktop" ) ,
85+ complete : async ( ) => {
86+ const result = await Promise . race ( [
87+ deepLink . complete . then ( ( data ) => ( {
88+ source : "deep-link" as const ,
89+ data,
90+ } ) ) ,
91+ localCallback . complete . then ( ( data ) => ( {
92+ source : "local" as const ,
93+ data,
94+ } ) ) ,
95+ ] ) ;
96+
97+ await deepLink . dispose ( ) ;
98+
99+ if ( result . source === "deep-link" ) {
100+ window . setTimeout ( ( ) => {
101+ void localCallback . dispose ( ) ;
102+ } , 10000 ) ;
103+ } else {
104+ await localCallback . dispose ( ) ;
105+ }
106+
107+ if ( ! result . data ) return null ;
108+ if ( signal . aborted ) throw new Error ( "Sign in aborted" ) ;
109+
110+ return result . data ;
111+ } ,
112+ } ;
113+ }
114+
115+ async function startLocalCallbackSession ( signal : AbortSignal ) {
52116 await invoke ( "plugin:oauth|stop" ) . catch ( ( ) => { } ) ;
53117
54118 const port : string = await invoke ( "plugin:oauth|start" , {
@@ -59,103 +123,90 @@ async function createLocalServerSession(signal: AbortSignal) {
59123 "Cache-Control" : "no-store, no-cache, must-revalidate" ,
60124 Pragma : "no-cache" ,
61125 } ,
62- // Add a cleanup function to stop the server after handling the request
63126 cleanup : true ,
64127 } ,
65128 } ) ;
66129
67- signal . onabort = ( ) => {
68- invoke ( "plugin:oauth|stop" ) . catch ( ( ) => { } ) ;
69- } ;
130+ let settled = false ;
131+ let stopListening : ( ( ) => void ) | undefined ;
132+ let resolvePromise : ( data : AuthParams | null ) => void = ( ) => { } ;
70133
71- let res : ( url : URL | null ) => void ;
72-
73- const stopListening = await listen (
74- "oauth://url" ,
75- ( data : { payload : string } ) => {
76- console . log ( data ) ;
77- if (
78- ! ( data . payload . includes ( "token" ) || data . payload . includes ( "api_key" ) )
79- ) {
80- return ;
81- }
82-
83- const urlObject = new URL ( data . payload ) ;
84- res ( urlObject ) ;
85- } ,
86- ) ;
134+ const complete = new Promise < AuthParams | null > ( ( resolve ) => {
135+ resolvePromise = resolve ;
136+ } ) ;
87137
88- signal . onabort = ( _e : Event ) => {
89- res ( null ) ;
138+ const settle = ( value : AuthParams | null ) => {
139+ if ( settled ) return ;
140+ settled = true ;
141+ resolvePromise ( value ) ;
90142 } ;
91143
92- return {
93- url : await createSessionRequestUrl ( port , "web" ) ,
94- complete : async ( ) => {
95- const url = await new Promise < URL | null > ( ( _res ) => {
96- res = _res ;
97- } ) ;
98-
99- stopListening ( ) ;
100- if ( ! url ) return null ;
101- if ( signal . aborted ) throw new Error ( "Sign in aborted" ) ;
144+ stopListening = await listen ( "oauth://url" , ( data : { payload : string } ) => {
145+ if ( ! ( data . payload . includes ( "token" ) || data . payload . includes ( "api_key" ) ) ) {
146+ return ;
147+ }
102148
103- const a = [ ...url . searchParams ] . reduce ( ( acc , [ k , v ] ) => {
104- acc [ k ] = v ;
105- return acc ;
106- } , { } as any ) ;
149+ settle ( parseAuthParams ( new URL ( data . payload ) ) ) ;
150+ } ) ;
107151
108- return paramsValidator . parse ( a ) ;
109- } ,
152+ const dispose = async ( ) => {
153+ stopListening ?.( ) ;
154+ stopListening = undefined ;
155+ settle ( null ) ;
156+ await invoke ( "plugin:oauth|stop" ) . catch ( ( ) => { } ) ;
110157 } ;
158+
159+ signal . addEventListener ( "abort" , ( ) => void dispose ( ) , { once : true } ) ;
160+
161+ return { port, complete, dispose } ;
111162}
112163
113- const paramsValidator = z . union ( [
114- z . object ( {
115- type : z . literal ( "api_key" ) ,
116- api_key : z . string ( ) ,
117- user_id : z . string ( ) ,
118- } ) ,
119- z . object ( {
120- token : z . string ( ) ,
121- user_id : z . string ( ) ,
122- expires : z . coerce . number ( ) ,
123- } ) ,
124- ] ) ;
164+ async function startDeepLinkSession ( signal : AbortSignal ) {
165+ let settled = false ;
166+ let stopListening : ( ( ) => void ) | undefined ;
167+ let resolvePromise : ( data : AuthParams | null ) => void = ( ) => { } ;
125168
126- async function createDeepLinkSession ( signal : AbortSignal ) {
127- let res : ( data : z . infer < typeof paramsValidator > ) => void ;
128- const p = new Promise < z . infer < typeof paramsValidator > > ( ( r ) => {
129- res = r ;
169+ const complete = new Promise < AuthParams | null > ( ( resolve ) => {
170+ resolvePromise = resolve ;
130171 } ) ;
131- const stopListening = await onOpenUrl ( async ( urls ) => {
132- for ( const urlString of urls ) {
133- if ( signal . aborted ) return ;
134172
135- const url = new URL ( urlString ) ;
173+ const settle = ( value : AuthParams | null ) => {
174+ if ( settled ) return ;
175+ settled = true ;
176+ resolvePromise ( value ) ;
177+ } ;
136178
137- res (
138- paramsValidator . parse (
139- [ ...url . searchParams ] . reduce ( ( acc , [ k , v ] ) => {
140- acc [ k ] = v ;
141- return acc ;
142- } , { } as any ) ,
143- ) ,
144- ) ;
179+ stopListening = await onOpenUrl ( async ( urls ) => {
180+ for ( const urlString of urls ) {
181+ if ( signal . aborted ) return ;
182+ settle ( parseAuthParams ( new URL ( urlString ) ) ) ;
145183 }
146184 } ) ;
147185
148- signal . onabort = ( ) => {
149- stopListening ( ) ;
186+ const dispose = async ( ) => {
187+ stopListening ?.( ) ;
188+ stopListening = undefined ;
189+ settle ( null ) ;
150190 } ;
151191
152- return {
153- url : await createSessionRequestUrl ( null , "desktop" ) ,
154- complete : ( ) => p ,
155- } ;
192+ signal . addEventListener ( "abort" , ( ) => void dispose ( ) , { once : true } ) ;
193+
194+ return { complete, dispose } ;
195+ }
196+
197+ function parseAuthParams ( url : URL ) {
198+ return paramsValidator . parse (
199+ [ ...url . searchParams ] . reduce (
200+ ( acc , [ key , value ] ) => {
201+ acc [ key ] = value ;
202+ return acc ;
203+ } ,
204+ { } as Record < string , string > ,
205+ ) ,
206+ ) ;
156207}
157208
158- async function processAuthData ( data : z . infer < typeof paramsValidator > ) {
209+ async function processAuthData ( data : AuthParams ) {
159210 identifyUser ( data . user_id ) ;
160211 trackEvent ( "user_signed_in" , { platform : "desktop" } ) ;
161212
0 commit comments