1- import { Component , Input , OnInit } from "@angular/core" ;
1+ import { ChangeDetectionStrategy , Component , input , OnDestroy , OnInit } from "@angular/core" ;
22import { FormControl , FormGroup , Validators } from "@angular/forms" ;
33import { map , Observable , of , startWith , Subject , takeUntil } from "rxjs" ;
44
55import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service" ;
66import { LogService } from "@bitwarden/common/platform/abstractions/log.service" ;
7+ import { Utils } from "@bitwarden/common/platform/misc/utils" ;
78import { PopoverModule , ToastService } from "@bitwarden/components" ;
89
910import { SharedModule } from "../../../shared" ;
@@ -34,18 +35,17 @@ type PaymentMethodFormGroup = FormGroup<{
3435 } > ;
3536} > ;
3637
37- // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
38- // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
3938@Component ( {
4039 selector : "app-enter-payment-method" ,
40+ changeDetection : ChangeDetectionStrategy . OnPush ,
4141 template : `
42- @let showBillingDetails = includeBillingAddress && selected !== "payPal";
43- <form [formGroup]="group">
42+ @let showBillingDetails = includeBillingAddress() && selected !== "payPal";
43+ <form [formGroup]="group() ">
4444 @if (showBillingDetails) {
4545 <h5 bitTypography="h5">{{ "paymentMethod" | i18n }}</h5>
4646 }
4747 <div class="tw-mb-4 tw-text-lg">
48- <bit-radio-group [formControl]="group.controls.type">
48+ <bit-radio-group [formControl]="group() .controls.type">
4949 <bit-radio-button id="card-payment-method" [value]="'card'">
5050 <bit-label>
5151 <i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
@@ -60,15 +60,15 @@ type PaymentMethodFormGroup = FormGroup<{
6060 </bit-label>
6161 </bit-radio-button>
6262 }
63- @if (showPayPal) {
63+ @if (showPayPal() ) {
6464 <bit-radio-button id="paypal-payment-method" [value]="'payPal'">
6565 <bit-label>
6666 <i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i>
6767 {{ "payPal" | i18n }}
6868 </bit-label>
6969 </bit-radio-button>
7070 }
71- @if (showAccountCredit) {
71+ @if (showAccountCredit() ) {
7272 <bit-radio-button id="credit-payment-method" [value]="'accountCredit'">
7373 <bit-label>
7474 <i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i>
@@ -82,10 +82,10 @@ type PaymentMethodFormGroup = FormGroup<{
8282 @case ("card") {
8383 <div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4">
8484 <div class="tw-col-span-1">
85- <app-payment-label for=" stripe-card-number" required>
85+ <app-payment-label [ for]="' stripe-card-number-' + instanceId " required>
8686 {{ "cardNumberLabel" | i18n }}
8787 </app-payment-label>
88- <div id=" stripe-card-number" class="tw-stripe-form-control"></div>
88+ <div [id]="' stripe-card-number-' + instanceId " class="tw-stripe-form-control"></div>
8989 </div>
9090 <div class="tw-col-span-1 tw-flex tw-items-end">
9191 <img
@@ -95,13 +95,13 @@ type PaymentMethodFormGroup = FormGroup<{
9595 />
9696 </div>
9797 <div class="tw-col-span-1">
98- <app-payment-label for=" stripe-card-expiry" required>
98+ <app-payment-label [ for]="' stripe-card-expiry-' + instanceId " required>
9999 {{ "expiration" | i18n }}
100100 </app-payment-label>
101- <div id=" stripe-card-expiry" class="tw-stripe-form-control"></div>
101+ <div [id]="' stripe-card-expiry-' + instanceId " class="tw-stripe-form-control"></div>
102102 </div>
103103 <div class="tw-col-span-1">
104- <app-payment-label for=" stripe-card-cvc" required>
104+ <app-payment-label [ for]="' stripe-card-cvc-' + instanceId " required>
105105 {{ "securityCodeSlashCVV" | i18n }}
106106 <button
107107 [bitPopoverTriggerFor]="cardSecurityCodePopover"
@@ -115,7 +115,7 @@ type PaymentMethodFormGroup = FormGroup<{
115115 <p class="tw-mb-0">{{ "cardSecurityCodeDescription" | i18n }}</p>
116116 </bit-popover>
117117 </app-payment-label>
118- <div id=" stripe-card-cvc" class="tw-stripe-form-control"></div>
118+ <div [id]="' stripe-card-cvc-' + instanceId " class="tw-stripe-form-control"></div>
119119 </div>
120120 </div>
121121 }
@@ -131,7 +131,7 @@ type PaymentMethodFormGroup = FormGroup<{
131131 bitInput
132132 id="routingNumber"
133133 type="text"
134- [formControl]="group.controls.bankAccount.controls.routingNumber"
134+ [formControl]="group() .controls.bankAccount.controls.routingNumber"
135135 required
136136 />
137137 </bit-form-field>
@@ -141,7 +141,7 @@ type PaymentMethodFormGroup = FormGroup<{
141141 bitInput
142142 id="accountNumber"
143143 type="text"
144- [formControl]="group.controls.bankAccount.controls.accountNumber"
144+ [formControl]="group() .controls.bankAccount.controls.accountNumber"
145145 required
146146 />
147147 </bit-form-field>
@@ -151,15 +151,15 @@ type PaymentMethodFormGroup = FormGroup<{
151151 id="accountHolderName"
152152 bitInput
153153 type="text"
154- [formControl]="group.controls.bankAccount.controls.accountHolderName"
154+ [formControl]="group() .controls.bankAccount.controls.accountHolderName"
155155 required
156156 />
157157 </bit-form-field>
158158 <bit-form-field class="tw-col-span-1" [disableMargin]="true">
159159 <bit-label>{{ "bankAccountType" | i18n }}</bit-label>
160160 <bit-select
161161 id="accountHolderType"
162- [formControl]="group.controls.bankAccount.controls.accountHolderType"
162+ [formControl]="group() .controls.bankAccount.controls.accountHolderType"
163163 required
164164 >
165165 <bit-option [value]="''" label="-- {{ 'select' | i18n }} --"></bit-option>
@@ -186,7 +186,7 @@ type PaymentMethodFormGroup = FormGroup<{
186186 }
187187 @case ("accountCredit") {
188188 <ng-container>
189- @if (hasEnoughAccountCredit) {
189+ @if (hasEnoughAccountCredit() ) {
190190 <bit-callout type="info">
191191 {{ "makeSureEnoughCredit" | i18n }}
192192 </bit-callout>
@@ -204,7 +204,7 @@ type PaymentMethodFormGroup = FormGroup<{
204204 <div class="tw-col-span-6">
205205 <bit-form-field [disableMargin]="true">
206206 <bit-label>{{ "country" | i18n }}</bit-label>
207- <bit-select [formControl]="group.controls.billingAddress.controls.country">
207+ <bit-select [formControl]="group() .controls.billingAddress.controls.country">
208208 @for (selectableCountry of selectableCountries; track selectableCountry.value) {
209209 <bit-option
210210 [value]="selectableCountry.value"
@@ -221,7 +221,7 @@ type PaymentMethodFormGroup = FormGroup<{
221221 <input
222222 bitInput
223223 type="text"
224- [formControl]="group.controls.billingAddress.controls.postalCode"
224+ [formControl]="group() .controls.billingAddress.controls.postalCode"
225225 autocomplete="postal-code"
226226 />
227227 </bit-form-field>
@@ -233,26 +233,15 @@ type PaymentMethodFormGroup = FormGroup<{
233233 standalone : true ,
234234 imports : [ BillingServicesModule , PaymentLabelComponent , PopoverModule , SharedModule ] ,
235235} )
236- export class EnterPaymentMethodComponent implements OnInit {
237- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
238- // eslint-disable-next-line @angular-eslint/prefer-signals
239- @Input ( { required : true } ) group ! : PaymentMethodFormGroup ;
236+ export class EnterPaymentMethodComponent implements OnInit , OnDestroy {
237+ protected readonly instanceId = Utils . newGuid ( ) ;
240238
241- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
242- // eslint-disable-next-line @angular-eslint/prefer-signals
243- @Input ( ) private showBankAccount = true ;
244- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
245- // eslint-disable-next-line @angular-eslint/prefer-signals
246- @Input ( ) showPayPal = true ;
247- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
248- // eslint-disable-next-line @angular-eslint/prefer-signals
249- @Input ( ) showAccountCredit = false ;
250- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
251- // eslint-disable-next-line @angular-eslint/prefer-signals
252- @Input ( ) hasEnoughAccountCredit = true ;
253- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
254- // eslint-disable-next-line @angular-eslint/prefer-signals
255- @Input ( ) includeBillingAddress = false ;
239+ readonly group = input . required < PaymentMethodFormGroup > ( ) ;
240+ protected readonly showBankAccount = input ( true ) ;
241+ readonly showPayPal = input ( true ) ;
242+ readonly showAccountCredit = input ( false ) ;
243+ readonly hasEnoughAccountCredit = input ( true ) ;
244+ readonly includeBillingAddress = input ( false ) ;
256245
257246 protected showBankAccount$ ! : Observable < boolean > ;
258247 protected selectableCountries = selectableCountries ;
@@ -269,57 +258,62 @@ export class EnterPaymentMethodComponent implements OnInit {
269258
270259 ngOnInit ( ) {
271260 this . stripeService . loadStripe (
261+ this . instanceId ,
272262 {
273- cardNumber : " #stripe-card-number" ,
274- cardExpiry : " #stripe-card-expiry" ,
275- cardCvc : " #stripe-card-cvc" ,
263+ cardNumber : ` #stripe-card-number- ${ this . instanceId } ` ,
264+ cardExpiry : ` #stripe-card-expiry- ${ this . instanceId } ` ,
265+ cardCvc : ` #stripe-card-cvc- ${ this . instanceId } ` ,
276266 } ,
277267 true ,
278268 ) ;
279269
280- if ( this . showPayPal ) {
270+ if ( this . showPayPal ( ) ) {
281271 this . braintreeService . loadBraintree ( "#braintree-container" , false ) ;
282272 }
283273
284- if ( ! this . includeBillingAddress ) {
285- this . showBankAccount$ = of ( this . showBankAccount ) ;
286- this . group . controls . billingAddress . disable ( ) ;
274+ if ( ! this . includeBillingAddress ( ) ) {
275+ this . showBankAccount$ = of ( this . showBankAccount ( ) ) ;
276+ this . group ( ) . controls . billingAddress . disable ( ) ;
287277 } else {
288- this . group . controls . billingAddress . patchValue ( {
278+ this . group ( ) . controls . billingAddress . patchValue ( {
289279 country : "US" ,
290280 } ) ;
291- this . showBankAccount$ = this . group . controls . billingAddress . controls . country . valueChanges . pipe (
292- startWith ( this . group . controls . billingAddress . controls . country . value ) ,
293- map ( ( country ) => this . showBankAccount && country === "US" ) ,
294- ) ;
281+ this . showBankAccount$ =
282+ this . group ( ) . controls . billingAddress . controls . country . valueChanges . pipe (
283+ startWith ( this . group ( ) . controls . billingAddress . controls . country . value ) ,
284+ map ( ( country ) => this . showBankAccount ( ) && country === "US" ) ,
285+ ) ;
295286 }
296287
297- this . group . controls . type . valueChanges
298- . pipe ( startWith ( this . group . controls . type . value ) , takeUntil ( this . destroy$ ) )
288+ this . group ( )
289+ . controls . type . valueChanges . pipe (
290+ startWith ( this . group ( ) . controls . type . value ) ,
291+ takeUntil ( this . destroy$ ) ,
292+ )
299293 . subscribe ( ( selected ) => {
300294 if ( selected === "bankAccount" ) {
301- this . group . controls . bankAccount . enable ( ) ;
302- if ( this . includeBillingAddress ) {
303- this . group . controls . billingAddress . enable ( ) ;
295+ this . group ( ) . controls . bankAccount . enable ( ) ;
296+ if ( this . includeBillingAddress ( ) ) {
297+ this . group ( ) . controls . billingAddress . enable ( ) ;
304298 }
305299 } else {
306300 switch ( selected ) {
307301 case "card" : {
308- this . stripeService . mountElements ( ) ;
309- if ( this . includeBillingAddress ) {
310- this . group . controls . billingAddress . enable ( ) ;
302+ this . stripeService . mountElements ( this . instanceId ) ;
303+ if ( this . includeBillingAddress ( ) ) {
304+ this . group ( ) . controls . billingAddress . enable ( ) ;
311305 }
312306 break ;
313307 }
314308 case "payPal" : {
315309 this . braintreeService . createDropin ( ) ;
316- if ( this . includeBillingAddress ) {
317- this . group . controls . billingAddress . disable ( ) ;
310+ if ( this . includeBillingAddress ( ) ) {
311+ this . group ( ) . controls . billingAddress . disable ( ) ;
318312 }
319313 break ;
320314 }
321315 }
322- this . group . controls . bankAccount . disable ( ) ;
316+ this . group ( ) . controls . bankAccount . disable ( ) ;
323317 }
324318 } ) ;
325319
@@ -330,22 +324,28 @@ export class EnterPaymentMethodComponent implements OnInit {
330324 } ) ;
331325 }
332326
327+ ngOnDestroy ( ) {
328+ this . stripeService . unloadStripe ( this . instanceId ) ;
329+ this . destroy$ . next ( ) ;
330+ this . destroy$ . complete ( ) ;
331+ }
332+
333333 select = ( paymentMethod : PaymentMethodOption ) =>
334- this . group . controls . type . patchValue ( paymentMethod ) ;
334+ this . group ( ) . controls . type . patchValue ( paymentMethod ) ;
335335
336336 tokenize = async ( ) : Promise < TokenizedPaymentMethod | null > => {
337337 const exchange = async ( paymentMethod : TokenizablePaymentMethod ) => {
338338 switch ( paymentMethod ) {
339339 case "bankAccount" : {
340- this . group . controls . bankAccount . markAllAsTouched ( ) ;
341- if ( ! this . group . controls . bankAccount . valid ) {
340+ this . group ( ) . controls . bankAccount . markAllAsTouched ( ) ;
341+ if ( ! this . group ( ) . controls . bankAccount . valid ) {
342342 throw new Error ( "Attempted to tokenize invalid bank account information." ) ;
343343 }
344344
345- const bankAccount = this . group . controls . bankAccount . getRawValue ( ) ;
345+ const bankAccount = this . group ( ) . controls . bankAccount . getRawValue ( ) ;
346346 const clientSecret = await this . stripeService . createSetupIntent ( "bankAccount" ) ;
347- const billingDetails = this . group . controls . billingAddress . enabled
348- ? this . group . controls . billingAddress . getRawValue ( )
347+ const billingDetails = this . group ( ) . controls . billingAddress . enabled
348+ ? this . group ( ) . controls . billingAddress . getRawValue ( )
349349 : undefined ;
350350 return await this . stripeService . setupBankAccountPaymentMethod (
351351 clientSecret ,
@@ -355,10 +355,14 @@ export class EnterPaymentMethodComponent implements OnInit {
355355 }
356356 case "card" : {
357357 const clientSecret = await this . stripeService . createSetupIntent ( "card" ) ;
358- const billingDetails = this . group . controls . billingAddress . enabled
359- ? this . group . controls . billingAddress . getRawValue ( )
358+ const billingDetails = this . group ( ) . controls . billingAddress . enabled
359+ ? this . group ( ) . controls . billingAddress . getRawValue ( )
360360 : undefined ;
361- return this . stripeService . setupCardPaymentMethod ( clientSecret , billingDetails ) ;
361+ return this . stripeService . setupCardPaymentMethod (
362+ this . instanceId ,
363+ clientSecret ,
364+ billingDetails ,
365+ ) ;
362366 }
363367 case "payPal" : {
364368 return this . braintreeService . requestPaymentMethod ( ) ;
@@ -410,15 +414,15 @@ export class EnterPaymentMethodComponent implements OnInit {
410414
411415 validate = ( ) : boolean => {
412416 if ( this . selected === "bankAccount" ) {
413- this . group . controls . bankAccount . markAllAsTouched ( ) ;
414- return this . group . controls . bankAccount . valid ;
417+ this . group ( ) . controls . bankAccount . markAllAsTouched ( ) ;
418+ return this . group ( ) . controls . bankAccount . valid ;
415419 }
416420
417421 return true ;
418422 } ;
419423
420424 get selected ( ) : PaymentMethodOption {
421- return this . group . value . type ! ;
425+ return this . group ( ) . value . type ! ;
422426 }
423427
424428 static getFormGroup = ( ) : PaymentMethodFormGroup =>
0 commit comments