Skip to content

Commit de42cf3

Browse files
[PM-27925] Refactor StripeService to allow more than one instance (#17467)
* Refactor StripeService to allow more than one instance per scope * Fix linting issue * Claude's feedback
1 parent 9ec05a9 commit de42cf3

File tree

4 files changed

+1087
-136
lines changed

4 files changed

+1087
-136
lines changed

apps/web/src/app/admin-console/settings/create-organization.component.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// FIXME: Update this file to be type safe and remove this and next line
22
// @ts-strict-ignore
3-
import { Component, OnInit } from "@angular/core";
4-
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
3+
import { Component, OnDestroy, OnInit } from "@angular/core";
54
import { ActivatedRoute } from "@angular/router";
5+
import { Subject, takeUntil } from "rxjs";
66
import { first } from "rxjs/operators";
77

88
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
@@ -19,7 +19,7 @@ import { SharedModule } from "../../shared";
1919
templateUrl: "create-organization.component.html",
2020
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
2121
})
22-
export class CreateOrganizationComponent implements OnInit {
22+
export class CreateOrganizationComponent implements OnInit, OnDestroy {
2323
protected secretsManager = false;
2424
protected plan: PlanType = PlanType.Free;
2525
protected productTier: ProductTierType = ProductTierType.Free;
@@ -29,6 +29,8 @@ export class CreateOrganizationComponent implements OnInit {
2929
private configService: ConfigService,
3030
) {}
3131

32+
private destroy$ = new Subject<void>();
33+
3234
async ngOnInit(): Promise<void> {
3335
const milestone3FeatureEnabled = await this.configService.getFeatureFlag(
3436
FeatureFlag.PM26462_Milestone_3,
@@ -37,7 +39,7 @@ export class CreateOrganizationComponent implements OnInit {
3739
? PlanType.FamiliesAnnually
3840
: PlanType.FamiliesAnnually2025;
3941

40-
this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => {
42+
this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
4143
if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) {
4244
this.plan = familyPlan;
4345
this.productTier = ProductTierType.Families;
@@ -61,4 +63,9 @@ export class CreateOrganizationComponent implements OnInit {
6163
this.secretsManager = qParams.product == ProductType.SecretsManager;
6264
});
6365
}
66+
67+
ngOnDestroy() {
68+
this.destroy$.next();
69+
this.destroy$.complete();
70+
}
6471
}

apps/web/src/app/billing/payment/components/enter-payment-method.component.ts

Lines changed: 79 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { Component, Input, OnInit } from "@angular/core";
1+
import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core";
22
import { FormControl, FormGroup, Validators } from "@angular/forms";
33
import { map, Observable, of, startWith, Subject, takeUntil } from "rxjs";
44

55
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
66
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
7+
import { Utils } from "@bitwarden/common/platform/misc/utils";
78
import { PopoverModule, ToastService } from "@bitwarden/components";
89

910
import { 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

Comments
 (0)