@@ -3,15 +3,16 @@ import type {
33 ISpendRequestResource ,
44 SpendRequest ,
55} from '@stripe/link-sdk' ;
6- import { Box , Text } from 'ink' ;
6+ import { Box , Text , useInput } from 'ink' ;
77import Spinner from 'ink-spinner' ;
88import type React from 'react' ;
9- import { useCallback , useEffect , useState } from 'react' ;
9+ import { useEffect , useState } from 'react' ;
1010import { DISPLAY_DELAY_MS } from '../../utils/constants' ;
1111import { writeCredentialFile } from '../../utils/credential-output' ;
12+ import { startCallbackServer } from '../../utils/local-callback-server' ;
13+ import { openUrl } from '../../utils/open-url' ;
1214import { AppDownloadQrCodes } from './app-download-qr-codes' ;
1315import { ApprovalWaitingView } from './approval-waiting-view' ;
14- import { useApprovalPolling } from './use-approval-polling' ;
1516
1617interface CreateSpendRequestProps {
1718 repository : ISpendRequestResource ;
@@ -31,52 +32,94 @@ export const CreateSpendRequest: React.FC<CreateSpendRequestProps> = ({
3132 onComplete,
3233} ) => {
3334 const [ status , setStatus ] = useState <
34- 'creating' | 'waiting' | 'polling' | ' success' | 'error'
35+ 'creating' | 'waiting' | 'success' | 'error'
3536 > ( 'creating' ) ;
3637 const [ request , setRequest ] = useState < SpendRequest | null > ( null ) ;
38+ const [ approvalUrl , setApprovalUrl ] = useState < string > ( '' ) ;
3739 const [ error , setError ] = useState < string > ( '' ) ;
3840 const [ outputFilePath , setOutputFilePath ] = useState < string | null > ( null ) ;
3941 const [ fileError , setFileError ] = useState < string > ( '' ) ;
4042
41- const approvalUrl = request ?. approval_url ?? '' ;
42-
43- const onSuccess = useCallback (
44- ( result : SpendRequest ) => setRequest ( result ) ,
45- [ ] ,
43+ useInput (
44+ ( _input , key ) => {
45+ if ( key . return && approvalUrl ) openUrl ( approvalUrl ) ;
46+ } ,
47+ { isActive : status === 'waiting' } ,
4648 ) ;
47- const onError = useCallback ( ( msg : string ) => setError ( msg ) , [ ] ) ;
48-
49- useApprovalPolling ( {
50- status,
51- setStatus,
52- approvalUrl,
53- repository,
54- requestId : request ?. id ?? null ,
55- onComplete,
56- onSuccess,
57- onError,
58- } ) ;
5949
6050 useEffect ( ( ) => {
51+ let close : ( ( ) => void ) | null = null ;
52+ let cancelled = false ;
53+
6154 const create = async ( ) => {
6255 try {
63- const result = await repository . createSpendRequest ( params ) ;
56+ let server : Awaited < ReturnType < typeof startCallbackServer > > | null =
57+ null ;
58+ if ( requestApproval ) {
59+ server = await startCallbackServer ( ) ;
60+ close = server . close ;
61+ }
62+
63+ // Strip request_approval so the spend request starts in `created` state; we call requestApproval explicitly below.
64+ const { request_approval : _ , ...createParams } = params ;
65+ const result = await repository . createSpendRequest ( createParams ) ;
66+ if ( cancelled ) return ;
6467 setRequest ( result ) ;
6568
66- if ( requestApproval ) {
67- setStatus ( 'waiting' ) ;
68- } else {
69+ if ( ! requestApproval || ! server ) {
6970 setStatus ( 'success' ) ;
7071 setTimeout ( ( ) => onComplete ( result ) , DISPLAY_DELAY_MS ) ;
72+ return ;
73+ }
74+
75+ const approval = await repository . requestApproval ( result . id , {
76+ redirect_uri : server . redirectUri ,
77+ } ) ;
78+ if ( cancelled ) return ;
79+
80+ setApprovalUrl ( approval . approval_link ) ;
81+ setStatus ( 'waiting' ) ;
82+
83+ const { approved } = await server . waitForCallback ( ) ;
84+ if ( cancelled ) return ;
85+
86+ const final = await repository . retrieve ( result . id ) ;
87+ if ( cancelled ) return ;
88+
89+ if ( ! final ) {
90+ setError ( 'Spend request not found after approval' ) ;
91+ setStatus ( 'error' ) ;
92+ setTimeout ( ( ) => onComplete ( null ) , DISPLAY_DELAY_MS ) ;
93+ return ;
94+ }
95+
96+ if ( approved && final . status === 'approved' ) {
97+ setRequest ( final ) ;
98+ setStatus ( 'success' ) ;
99+ setTimeout ( ( ) => onComplete ( final ) , DISPLAY_DELAY_MS ) ;
100+ } else {
101+ setError (
102+ `Spend request did not reach approved (status: ${ final . status } )` ,
103+ ) ;
104+ setStatus ( 'error' ) ;
105+ setTimeout ( ( ) => onComplete ( final ) , DISPLAY_DELAY_MS ) ;
71106 }
72107 } catch ( err ) {
73- setError ( ( err as Error ) . message ) ;
74- setStatus ( 'error' ) ;
75- setTimeout ( ( ) => onComplete ( null ) , DISPLAY_DELAY_MS ) ;
108+ if ( ! cancelled ) {
109+ setError ( ( err as Error ) . message ) ;
110+ setStatus ( 'error' ) ;
111+ setTimeout ( ( ) => onComplete ( null ) , DISPLAY_DELAY_MS ) ;
112+ }
113+ } finally {
114+ close ?.( ) ;
76115 }
77116 } ;
78117
79118 create ( ) ;
119+ return ( ) => {
120+ cancelled = true ;
121+ close ?.( ) ;
122+ } ;
80123 } , [ repository , params , requestApproval , onComplete ] ) ;
81124
82125 useEffect ( ( ) => {
@@ -206,10 +249,7 @@ export const CreateSpendRequest: React.FC<CreateSpendRequestProps> = ({
206249 ✓ Spend request created (ID: < Text bold > { request ?. id } </ Text > )
207250 </ Text >
208251 </ Box >
209- < ApprovalWaitingView
210- status = { status as 'waiting' | 'polling' }
211- approvalUrl = { approvalUrl }
212- />
252+ < ApprovalWaitingView approvalUrl = { approvalUrl } />
213253 </ >
214254 ) ;
215255} ;
0 commit comments