From 70fbcf2a10aa9de6ed7a598e157852e2f68665f9 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:37:44 -0500 Subject: [PATCH] [PM-11657] Stripe + Browser Refresh Styling (#10978) * add check for `ExtensionRefresh` in StripeService - Stripe components need new styles to match the new CL components * add global styles for Stripe components - Matches closer to the browser refresh components * add browser refresh component details to Stripe JS initialization * add component to match the display of the new component library that shows only when the `ExtensionRefresh` flag is enabled * update both payment components to use payment label component - This styling of the label is separate from the `AC2476_DeprecateStripeSourcesAPI` flag * update security code copy * change layout of the trial component to account for new CL components * absolutely position label to remove extra spacing around the label * remove unneeded logic --- .../app/billing/services/stripe.service.ts | 41 +++++++++++++++---- .../payment/payment-label-v2.component.html | 22 ++++++++++ .../payment/payment-label-v2.component.ts | 36 ++++++++++++++++ .../shared/payment/payment-v2.component.html | 22 ++++------ .../shared/payment/payment-v2.component.ts | 4 +- .../shared/payment/payment.component.html | 24 ++++++----- .../shared/payment/payment.component.ts | 25 ++++++++--- .../billing/shared/tax-info.component.html | 4 +- apps/web/src/locales/en/messages.json | 3 ++ apps/web/src/scss/forms.scss | 26 +++++++++++- 10 files changed, 166 insertions(+), 41 deletions(-) create mode 100644 apps/web/src/app/billing/shared/payment/payment-label-v2.component.html create mode 100644 apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts diff --git a/apps/web/src/app/billing/services/stripe.service.ts b/apps/web/src/app/billing/services/stripe.service.ts index 0445504d50..d19a7ef9da 100644 --- a/apps/web/src/app/billing/services/stripe.service.ts +++ b/apps/web/src/app/billing/services/stripe.service.ts @@ -1,6 +1,8 @@ import { Injectable } from "@angular/core"; import { BankAccount } from "@bitwarden/common/billing/models/domain"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { BillingServicesModule } from "./billing-services.module"; @@ -15,7 +17,10 @@ export class StripeService { cardCvc: string; }; - constructor(private logService: LogService) {} + constructor( + private logService: LogService, + private configService: ConfigService, + ) {} /** * Loads [Stripe JS]{@link https://docs.stripe.com/js} in the element of the current page and mounts @@ -32,15 +37,23 @@ export class StripeService { const script = window.document.createElement("script"); script.id = "stripe-script"; script.src = "https://js.stripe.com/v3?advancedFraudSignals=false"; - script.onload = () => { + script.onload = async () => { const window$ = window as any; this.stripe = window$.Stripe(process.env.STRIPE_KEY); this.elements = this.stripe.elements(); - const options = this.getElementOptions(); + const isExtensionRefresh = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); setTimeout(() => { - this.elements.create("cardNumber", options); - this.elements.create("cardExpiry", options); - this.elements.create("cardCvc", options); + this.elements.create( + "cardNumber", + this.getElementOptions("cardNumber", isExtensionRefresh), + ); + this.elements.create( + "cardExpiry", + this.getElementOptions("cardExpiry", isExtensionRefresh), + ); + this.elements.create("cardCvc", this.getElementOptions("cardCvc", isExtensionRefresh)); if (autoMount) { this.mountElements(); } @@ -135,7 +148,10 @@ export class StripeService { }, 500); } - private getElementOptions(): any { + private getElementOptions( + element: "cardNumber" | "cardExpiry" | "cardCvc", + isExtensionRefresh: boolean, + ): any { const options: any = { style: { base: { @@ -160,6 +176,17 @@ export class StripeService { }, }; + // Unique settings that should only be applied when the extension refresh flag is active + if (isExtensionRefresh) { + options.style.base.fontWeight = "500"; + options.classes.base = "v2"; + + // Remove the placeholder for number and CVC fields + if (["cardNumber", "cardCvc"].includes(element)) { + options.placeholder = ""; + } + } + const style = getComputedStyle(document.documentElement); options.style.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; options.style.base["::placeholder"].color = `rgb(${style.getPropertyValue( diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html new file mode 100644 index 0000000000..a2e1c92a05 --- /dev/null +++ b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.html @@ -0,0 +1,22 @@ + + + + + +
+ + + ({{ "required" | i18n }}) + +
+
+ + + + diff --git a/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts new file mode 100644 index 0000000000..4e671ed593 --- /dev/null +++ b/apps/web/src/app/billing/shared/payment/payment-label-v2.component.ts @@ -0,0 +1,36 @@ +import { booleanAttribute, Component, Input, OnInit } from "@angular/core"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { FormFieldModule } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +/** + * Label that should be used for elements loaded via Stripe API. + * + * Applies the same label styles from CL form-field component when + * the `ExtensionRefresh` flag is set. + */ +@Component({ + selector: "app-payment-label-v2", + templateUrl: "./payment-label-v2.component.html", + standalone: true, + imports: [FormFieldModule, SharedModule], +}) +export class PaymentLabelV2 implements OnInit { + /** `id` of the associated input */ + @Input({ required: true }) for: string; + /** Displays required text on the label */ + @Input({ transform: booleanAttribute }) required = false; + + protected extensionRefreshFlag = false; + + constructor(private configService: ConfigService) {} + + async ngOnInit(): Promise { + this.extensionRefreshFlag = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + } +} diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.html b/apps/web/src/app/billing/shared/payment/payment-v2.component.html index e701c1ce67..51fdb1738f 100644 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.html +++ b/apps/web/src/app/billing/shared/payment/payment-v2.component.html @@ -43,10 +43,9 @@
- +
@@ -57,29 +56,24 @@ />
- +
-
- + + {{ "securityCodeSlashCVV" | i18n }} -
+
diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts index fa2b53fc7b..6c12b14cf0 100644 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts @@ -10,6 +10,8 @@ import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/ import { SharedModule } from "../../../shared"; import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; +import { PaymentLabelV2 } from "./payment-label-v2.component"; + /** * Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and, * optionally, submit it using the {@link onSubmit} function if it is provided. @@ -20,7 +22,7 @@ import { BillingServicesModule, BraintreeService, StripeService } from "../../se selector: "app-payment-v2", templateUrl: "./payment-v2.component.html", standalone: true, - imports: [BillingServicesModule, SharedModule], + imports: [BillingServicesModule, SharedModule, PaymentLabelV2], }) export class PaymentV2Component implements OnInit, OnDestroy { /** Show account credit as a payment option. */ diff --git a/apps/web/src/app/billing/shared/payment/payment.component.html b/apps/web/src/app/billing/shared/payment/payment.component.html index 4e71df2eff..a627da18da 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.html +++ b/apps/web/src/app/billing/shared/payment/payment.component.html @@ -26,8 +26,10 @@
-
- +
+ {{ + "number" | i18n + }}
@@ -38,26 +40,26 @@ height="32" />
-
- +
+ {{ + "expiration" | i18n + }}
-
-
- +
+ + {{ "securityCodeSlashCVV" | i18n }} -
+
diff --git a/apps/web/src/app/billing/shared/payment/payment.component.ts b/apps/web/src/app/billing/shared/payment/payment.component.ts index 03269c70a5..ab81a602d2 100644 --- a/apps/web/src/app/billing/shared/payment/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment.component.ts @@ -5,15 +5,19 @@ import { Subject, takeUntil } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SharedModule } from "../../../shared"; +import { PaymentLabelV2 } from "./payment-label-v2.component"; + @Component({ selector: "app-payment", templateUrl: "payment.component.html", standalone: true, - imports: [SharedModule], + imports: [SharedModule, PaymentLabelV2], }) export class PaymentComponent implements OnInit, OnDestroy { @Input() showMethods = true; @@ -63,14 +67,15 @@ export class PaymentComponent implements OnInit, OnDestroy { private apiService: ApiService, private logService: LogService, private themingService: AbstractThemingService, + private configService: ConfigService, ) { this.stripeScript = window.document.createElement("script"); this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; this.stripeScript.async = true; - this.stripeScript.onload = () => { + this.stripeScript.onload = async () => { this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); this.stripeElements = this.stripe.elements(); - this.setStripeElement(); + await this.setStripeElement(); }; this.btScript = window.document.createElement("script"); this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; @@ -187,7 +192,7 @@ export class PaymentComponent implements OnInit, OnDestroy { ); }, 250); } else { - this.setStripeElement(); + void this.setStripeElement(); } } @@ -267,7 +272,17 @@ export class PaymentComponent implements OnInit, OnDestroy { }); } - private setStripeElement() { + private async setStripeElement() { + const extensionRefreshFlag = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + + // Apply unique styles for extension refresh + if (extensionRefreshFlag) { + this.StripeElementStyle.base.fontWeight = "500"; + this.StripeElementClasses.base = "v2"; + } + window.setTimeout(() => { if (this.showMethods && this.method === PaymentMethodType.Card) { if (this.stripeCardNumberElement == null) { diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index 89bc7438a7..82d5104a53 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -1,6 +1,6 @@
-
+
{{ "country" | i18n }} @@ -13,7 +13,7 @@
-
+
{{ "zipPostalCode" | i18n }} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a9a709f3ec..38c58d9b8a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -143,6 +143,9 @@ "securityCode": { "message": "Security code (CVV)" }, + "securityCodeSlashCVV": { + "message": "Security code / CVV" + }, "identityName": { "message": "Identity name" }, diff --git a/apps/web/src/scss/forms.scss b/apps/web/src/scss/forms.scss index 9404bc9403..f5800ff7e5 100644 --- a/apps/web/src/scss/forms.scss +++ b/apps/web/src/scss/forms.scss @@ -98,7 +98,7 @@ input[type="checkbox"] { cursor: pointer; } -.form-control.stripe-form-control { +.form-control.stripe-form-control:not(.v2) { padding-top: 0.55rem; &.is-focused { @@ -126,6 +126,30 @@ input[type="checkbox"] { } } +.form-control.stripe-form-control.v2 { + padding: 0.6875rem 0.875rem; + border-radius: 0.5rem; + border-color: rgb(var(--color-text-muted)); + height: unset; + font-weight: 500; + color: rgb(var(--color-text-main)); + background-color: rgb(var(--color-background)); + + &:hover { + border-color: rgb(var(--color-primary-500)); + } + + &.is-focused { + outline: 0; + border-color: rgb(var(--color-primary-500)); + } + + &.is-invalid { + color: rgb(var(--color-text-main)); + border-color: rgb(var(--color-danger-600)); + } +} + .dropdown-menu, .dropdown-item { @include themify($themes) {