@@ -3,29 +3,144 @@ import { tracked } from '@glimmer/tracking';
33import { inject as service } from '@ember/service' ;
44import { action } from '@ember/object' ;
55import { task } from 'ember-concurrency' ;
6+ import copyToClipboard from '@fleetbase/ember-core/utils/copy-to-clipboard' ;
67
78export default class GatewayFormComponent extends Component {
89 @service fetch ;
910 @service notifications ;
1011
1112 @tracked availableDrivers = [ ] ;
13+ @tracked activeStep = 0 ;
1214 @tracked configSchema = [ ] ;
1315 @tracked configValues = { } ;
16+ @tracked connectionTestResult ;
1417
1518 constructor ( ) {
1619 super ( ...arguments ) ;
1720 this . loadDrivers . perform ( ) ;
1821 }
1922
23+ get setupSteps ( ) {
24+ const steps = [
25+ { icon : 'key' , label : 'Credentials' , complete : this . hasRequiredConfig } ,
26+ { icon : 'route' , label : 'Routing' , complete : Boolean ( this . args . resource ?. environment && this . args . resource ?. status ) } ,
27+ { icon : 'circle-check' , label : 'Review' , complete : Boolean ( this . args . resource ?. name && this . args . resource ?. driver ) } ,
28+ ] ;
29+
30+ if ( this . canSelectDriver ) {
31+ steps . unshift ( { icon : 'plug' , label : 'Gateway' , complete : Boolean ( this . selectedDriver ) } ) ;
32+ }
33+
34+ return steps . map ( ( step , index ) => ( {
35+ ...step ,
36+ key : step . label . toLowerCase ( ) ,
37+ active : this . activeStep === index ,
38+ complete : index < this . activeStep && step . complete ,
39+ } ) ) ;
40+ }
41+
42+ get currentStep ( ) {
43+ return this . setupSteps [ this . activeStep ] ?. key ?? this . setupSteps [ 0 ] ?. key ;
44+ }
45+
46+ get canSelectDriver ( ) {
47+ return ! this . args . resource ?. id ;
48+ }
49+
50+ get driverCards ( ) {
51+ return this . availableDrivers . map ( ( driver ) => ( {
52+ ...driver ,
53+ selected : driver . code === this . args . resource ?. driver ,
54+ categoryLabel : driver . category ?? this . driverCategory ( driver . code ) ,
55+ icon : this . driverIcon ( driver . code ) ,
56+ } ) ) ;
57+ }
58+
59+ get selectedDriver ( ) {
60+ return this . availableDrivers . find ( ( driver ) => driver . code === this . args . resource ?. driver ) ;
61+ }
62+
63+ get selectedDriverIcon ( ) {
64+ return this . driverIcon ( this . selectedDriver ?. code ) ;
65+ }
66+
67+ get hasRequiredConfig ( ) {
68+ const requiredFields = this . configSchema . filter ( ( field ) => field . required ) ;
69+
70+ return requiredFields . every ( ( field ) => {
71+ const value = this . configValues [ field . key ] ;
72+ return value !== null && value !== undefined && String ( value ) . trim ( ) !== '' ;
73+ } ) ;
74+ }
75+
76+ get connectionState ( ) {
77+ if ( this . testCredentials . isRunning ) {
78+ return 'testing' ;
79+ }
80+
81+ if ( ! this . connectionTestResult ) {
82+ return 'idle' ;
83+ }
84+
85+ return this . connectionTestResult . success ? 'success' : 'failed' ;
86+ }
87+
88+ get connectionStateTitle ( ) {
89+ switch ( this . connectionState ) {
90+ case 'testing' :
91+ return 'Testing gateway credentials...' ;
92+ case 'success' :
93+ return 'Gateway verified' ;
94+ case 'failed' :
95+ return 'Gateway test failed' ;
96+ default :
97+ return this . args . resource ?. id ? 'Ready to verify' : 'Save to verify' ;
98+ }
99+ }
100+
101+ get connectionStateMessage ( ) {
102+ if ( ! this . args . resource ?. id ) {
103+ return 'Create the gateway first, then run live credential checks and webhook registration from the gateway diagnostics screen.' ;
104+ }
105+
106+ if ( this . connectionState === 'idle' ) {
107+ return 'Run a credential test before using this gateway for invoice payments.' ;
108+ }
109+
110+ if ( this . connectionState === 'testing' ) {
111+ return 'Ledger is checking the configured credentials with the payment provider.' ;
112+ }
113+
114+ return this . connectionTestResult ?. message ?? 'The gateway credential check completed.' ;
115+ }
116+
117+ get connectionMetadataEntries ( ) {
118+ return Object . entries ( this . connectionTestResult ?. metadata ?? { } ) . map ( ( [ key , value ] ) => ( {
119+ label : key . replaceAll ( '_' , ' ' ) ,
120+ value,
121+ } ) ) ;
122+ }
123+
124+ get reviewConfigEntries ( ) {
125+ return this . configSchema . map ( ( field ) => ( {
126+ label : field . label ,
127+ value : field . type === 'password' && this . configValues [ field . key ] ? '••••••••' : this . configValues [ field . key ] ,
128+ } ) ) ;
129+ }
130+
20131 @task * loadDrivers ( ) {
21132 try {
22133 const response = yield this . fetch . get ( 'gateways/drivers' , { } , { namespace : 'ledger/int/v1' } ) ;
23134 // The endpoint returns { status: 'ok', drivers: [...] }
24135 this . availableDrivers = response ?. drivers ?? response ?? [ ] ;
25136
26- // If the resource already has a driver selected, load its schema
27137 if ( this . args . resource ?. driver ) {
28138 yield this . loadSchema . perform ( this . args . resource . driver ) ;
139+ } else {
140+ const initialDriver = this . args . initialDriverCode ? this . availableDrivers . find ( ( driver ) => driver . code === this . args . initialDriverCode ) : this . availableDrivers [ 0 ] ;
141+ if ( initialDriver ) {
142+ this . selectDriver ( initialDriver ) ;
143+ }
29144 }
30145 } catch ( err ) {
31146 this . notifications . warning ( 'Could not load available payment drivers.' ) ;
@@ -56,21 +171,97 @@ export default class GatewayFormComponent extends Component {
56171 }
57172
58173 @action selectDriver ( driver ) {
59- this . args . resource . driver = driver . code ;
174+ if ( ! this . canSelectDriver ) {
175+ return ;
176+ }
177+
178+ this . args . resource . set ?. ( 'driver' , driver . code ) ;
179+ this . args . resource . set ?. ( 'name' , this . args . resource ?. name || driver . name ) ;
180+ this . args . resource . set ?. ( 'code' , this . args . resource ?. code || driver . code ) ;
60181 // Sync the driver's capabilities to the resource so they are persisted
61182 if ( driver . capabilities ) {
62- this . args . resource . capabilities = driver . capabilities ;
183+ this . args . resource . set ?. ( ' capabilities' , driver . capabilities ) ;
63184 }
64185 this . loadSchema . perform ( driver . code ) ;
186+ this . connectionTestResult = null ;
65187 }
66188
67189 @action updateConfigField ( key , value ) {
68190 // When bound via {{on "input" (fn this.updateConfigField field.key)}} the second
69191 // argument is a DOM InputEvent, not the raw string value. Extract the actual
70192 // value from event.target.value in that case so credentials are stored correctly.
71- const resolvedValue = value instanceof Event ? value . target ?. value ?? value : value ;
193+ const resolvedValue = value instanceof Event ? ( value . target ?. value ?? value ) : ( value ?. value ?? value ) ;
72194 this . configValues = { ...this . configValues , [ key ] : resolvedValue } ;
73195 // Persist config back to the resource
74- this . args . resource . config = { ...this . configValues } ;
196+ this . args . resource . set ?. ( 'config' , { ...this . configValues } ) ;
197+ this . connectionTestResult = null ;
198+ }
199+
200+ @task * testCredentials ( ) {
201+ if ( ! this . args . resource ?. id ) {
202+ this . notifications . info ( 'Save this gateway before testing credentials.' ) ;
203+ return ;
204+ }
205+
206+ try {
207+ this . connectionTestResult = yield this . fetch . post ( `gateways/${ this . args . resource . id } /test-credentials` , { } , { namespace : 'ledger/int/v1' } ) ;
208+ } catch ( error ) {
209+ this . connectionTestResult = {
210+ success : false ,
211+ message : error . message ?? 'Gateway credential test failed.' ,
212+ metadata : { } ,
213+ } ;
214+ this . notifications . serverError ( error ) ;
215+ }
216+ }
217+
218+ @action setResourceField ( field , value ) {
219+ const resolvedValue = value instanceof Event ? ( value . target ?. value ?? value ) : ( value ?. value ?? value ) ;
220+ this . args . resource ?. set ?. ( field , resolvedValue ) ;
221+ }
222+
223+ @action goToStep ( index ) {
224+ this . activeStep = index ;
225+ }
226+
227+ @action nextStep ( ) {
228+ this . activeStep = Math . min ( this . activeStep + 1 , this . setupSteps . length - 1 ) ;
229+ }
230+
231+ @action previousStep ( ) {
232+ this . activeStep = Math . max ( this . activeStep - 1 , 0 ) ;
233+ }
234+
235+ @action copyWebhookUrl ( ) {
236+ const url = this . args . resource ?. system_webhook_url ?? this . args . resource ?. webhook_url ;
237+ if ( ! url ) {
238+ return this . notifications . warning ( 'No webhook URL is available yet.' ) ;
239+ }
240+
241+ copyToClipboard ( url )
242+ . then ( ( ) => this . notifications . success ( 'Webhook URL copied.' ) )
243+ . catch ( ( ) => this . notifications . error ( 'Unable to copy webhook URL.' ) ) ;
244+ }
245+
246+ driverCategory ( code ) {
247+ return (
248+ {
249+ taler : 'Wallet payment' ,
250+ stripe : 'Cards and wallets' ,
251+ cash : 'Manual payment' ,
252+ qpay : 'Regional payment' ,
253+ } [ code ] ?? 'Payment gateway'
254+ ) ;
255+ }
256+
257+ driverIcon ( code ) {
258+ return (
259+ {
260+ taler : 'wallet' ,
261+ stripe : 'credit-card' ,
262+ cash : 'money-bill-wave' ,
263+ qpay : 'qrcode' ,
264+ } [ code ] ?? 'plug'
265+ ) ;
75266 }
76267}
0 commit comments