@@ -11,6 +11,7 @@ import type {
1111 ConnectWecomInput ,
1212 DesktopRewardClaimProof ,
1313 DesktopRewardsStatus ,
14+ RewardTask ,
1415 RewardTaskId ,
1516} from "@nexu/shared" ;
1617import {
@@ -23,6 +24,7 @@ import {
2324 type refreshIntegrationSchema ,
2425 rewardGroupSchema ,
2526 rewardTaskIdSchema ,
27+ rewardTasks ,
2628 type updateAuthSourceSchema ,
2729 type updateUserProfileSchema ,
2830 type upsertProviderBodySchema ,
@@ -94,6 +96,10 @@ const defaultCloudProfile: CloudProfileEntry = {
9496 linkUrl : "https://link.nexu.io" ,
9597} ;
9698
99+ const rewardTaskTemplateById = new Map < RewardTaskId , RewardTask > (
100+ rewardTasks . map ( ( task ) => [ task . id , task ] ) ,
101+ ) ;
102+
97103export type DesktopCloudStateChange = {
98104 hadCloudInventory : boolean ;
99105 hasCloudInventory : boolean ;
@@ -312,6 +318,29 @@ function convertCloudStatusToDesktop(
312318 } ,
313319) : DesktopRewardsStatus {
314320 const { cloudConnected, activeModelId, activeManagedModel } = viewer ;
321+ const tasks = cloudStatus . tasks . flatMap ( ( task ) => {
322+ const parsedTaskId = rewardTaskIdSchema . safeParse ( task . id ) ;
323+ const parsedGroupId = rewardGroupSchema . safeParse ( task . groupId ) ;
324+ if ( ! parsedTaskId . success || ! parsedGroupId . success ) {
325+ return [ ] ;
326+ }
327+
328+ return {
329+ id : parsedTaskId . data as RewardTaskId ,
330+ group : parsedGroupId . data ,
331+ icon : task . icon ?? "gift" ,
332+ reward : task . rewardPoints ,
333+ shareMode : task . shareMode as "link" | "tweet" | "image" ,
334+ repeatMode : task . repeatMode as "once" | "daily" | "weekly" ,
335+ requiresScreenshot : task . shareMode === "image" ,
336+ actionUrl :
337+ rewardTaskTemplateById . get ( parsedTaskId . data ) ?. actionUrl ?? null ,
338+ isClaimed : task . isClaimed ,
339+ lastClaimedAt : task . lastClaimedAt ,
340+ claimCount : task . claimCount ,
341+ } ;
342+ } ) ;
343+
315344 return {
316345 viewer : {
317346 cloudConnected,
@@ -321,28 +350,12 @@ function convertCloudStatusToDesktop(
321350 ( activeManagedModel ? "nexu" : ( activeModelId ?. split ( "/" ) [ 0 ] ?? null ) ) ,
322351 usingManagedModel : activeManagedModel != null ,
323352 } ,
324- progress : cloudStatus . progress ,
325- tasks : cloudStatus . tasks . flatMap ( ( task ) => {
326- const parsedTaskId = rewardTaskIdSchema . safeParse ( task . id ) ;
327- const parsedGroupId = rewardGroupSchema . safeParse ( task . groupId ) ;
328- if ( ! parsedTaskId . success || ! parsedGroupId . success ) {
329- return [ ] ;
330- }
331-
332- return {
333- id : parsedTaskId . data as RewardTaskId ,
334- group : parsedGroupId . data ,
335- icon : task . icon ?? "gift" ,
336- reward : task . rewardPoints ,
337- shareMode : task . shareMode as "link" | "tweet" | "image" ,
338- repeatMode : task . repeatMode as "once" | "daily" | "weekly" ,
339- requiresScreenshot : task . shareMode === "image" ,
340- actionUrl : task . url ,
341- isClaimed : task . isClaimed ,
342- lastClaimedAt : task . lastClaimedAt ,
343- claimCount : task . claimCount ,
344- } ;
345- } ) ,
353+ progress : {
354+ ...cloudStatus . progress ,
355+ claimedCount : tasks . filter ( ( task ) => task . isClaimed ) . length ,
356+ totalCount : tasks . length ,
357+ } ,
358+ tasks,
346359 cloudBalance : cloudStatus . cloudBalance
347360 ? {
348361 totalBalance : cloudStatus . cloudBalance . totalBalance ,
@@ -538,6 +551,15 @@ export class NexuConfigStore {
538551 } ) ;
539552 }
540553
554+ private isCurrentPollingSignal ( signal : AbortSignal ) : boolean {
555+ // The polling loop may still be processing a response when a newer
556+ // connectDesktopCloud() call has already aborted it and installed a fresh
557+ // pollingState. Identifying the active poll by AbortSignal identity lets
558+ // any final-state write from a stale loop become a no-op instead of
559+ // clobbering the new flow's pollingState or persisted credentials.
560+ return this . pollingState ?. abortController . signal === signal ;
561+ }
562+
541563 private async pollDesktopCloudAuthorization (
542564 cloudApiUrl : string ,
543565 deviceId : string ,
@@ -584,6 +606,9 @@ export class NexuConfigStore {
584606 : ( ( await this . fetchDesktopCloudModels ( linkUrl , data . apiKey ) ) ??
585607 [ ] ) ;
586608
609+ if ( signal . aborted || ! this . isCurrentPollingSignal ( signal ) ) {
610+ return ;
611+ }
587612 this . pollingState = null ;
588613 await this . setDesktopCloudState ( {
589614 connected : true ,
@@ -605,6 +630,9 @@ export class NexuConfigStore {
605630 }
606631
607632 if ( data . status === "expired" ) {
633+ if ( signal . aborted || ! this . isCurrentPollingSignal ( signal ) ) {
634+ return ;
635+ }
608636 this . pollingState = null ;
609637 await this . setDesktopCloudState ( {
610638 connected : false ,
@@ -626,6 +654,9 @@ export class NexuConfigStore {
626654 }
627655 }
628656
657+ if ( signal . aborted || ! this . isCurrentPollingSignal ( signal ) ) {
658+ return ;
659+ }
629660 this . pollingState = null ;
630661 await this . setDesktopCloudState ( {
631662 connected : false ,
@@ -1815,17 +1846,38 @@ export class NexuConfigStore {
18151846 } ) ;
18161847 }
18171848
1849+ private abortDesktopCloudPolling ( ) : void {
1850+ if ( this . pollingState ) {
1851+ this . pollingState . abortController . abort ( ) ;
1852+ this . pollingState = null ;
1853+ }
1854+ }
1855+
18181856 async connectDesktopCloud ( options ?: { source ?: string | null } ) {
18191857 const config = await this . getConfig ( ) ;
18201858 const current = readDesktopCloud ( config ) ;
18211859 const { activeProfile } =
18221860 await this . readConfiguredDesktopCloudProfile ( config ) ;
1823- if ( this . pollingState || current . polling ) {
1824- return { error : "Connection attempt already in progress" } ;
1825- }
18261861 if ( current . connected && current . apiKey ) {
18271862 return { error : "Already connected. Disconnect first." } ;
18281863 }
1864+ // If a previous connect attempt is still polling (e.g. the user closed the
1865+ // authorization tab without completing the flow), cancel it and clear the
1866+ // persisted polling flag so this call can start a fresh browser login.
1867+ if ( this . pollingState || current . polling ) {
1868+ this . abortDesktopCloudPolling ( ) ;
1869+ await this . setDesktopCloudState ( {
1870+ connected : false ,
1871+ polling : false ,
1872+ userId : null ,
1873+ userName : null ,
1874+ userEmail : null ,
1875+ connectedAt : null ,
1876+ linkUrl : null ,
1877+ apiKey : null ,
1878+ models : [ ] ,
1879+ } ) ;
1880+ }
18291881 const trimmedSource = options ?. source ?. trim ( ) ;
18301882 const sourceQuery =
18311883 trimmedSource && trimmedSource . length > 0
@@ -2040,10 +2092,7 @@ export class NexuConfigStore {
20402092
20412093 const previousCloud = readDesktopCloud ( await this . getConfig ( ) ) ;
20422094
2043- if ( this . pollingState ) {
2044- this . pollingState . abortController . abort ( ) ;
2045- this . pollingState = null ;
2046- }
2095+ this . abortDesktopCloudPolling ( ) ;
20472096
20482097 await this . store . update ( ( config ) => {
20492098 const currentProfile = readLocalProfile ( config ) ;
@@ -2107,10 +2156,7 @@ export class NexuConfigStore {
21072156 throw new Error ( `Unknown cloud profile: ${ name } ` ) ;
21082157 }
21092158
2110- if ( this . pollingState ) {
2111- this . pollingState . abortController . abort ( ) ;
2112- this . pollingState = null ;
2113- }
2159+ this . abortDesktopCloudPolling ( ) ;
21142160
21152161 await this . store . update ( ( currentConfig ) => {
21162162 const sessions = readDesktopCloudSessions ( currentConfig ) ;
@@ -2252,10 +2298,7 @@ export class NexuConfigStore {
22522298
22532299 async disconnectDesktopCloud ( ) {
22542300 const previousCloud = readDesktopCloud ( await this . getConfig ( ) ) ;
2255- if ( this . pollingState ) {
2256- this . pollingState . abortController . abort ( ) ;
2257- this . pollingState = null ;
2258- }
2301+ this . abortDesktopCloudPolling ( ) ;
22592302
22602303 await this . setDesktopCloudState ( {
22612304 connected : false ,
0 commit comments