1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

[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
This commit is contained in:
Nick Krantz 2024-09-12 14:37:44 -05:00 committed by GitHub
parent b6cde7e3ef
commit 70fbcf2a10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 166 additions and 41 deletions

View File

@ -1,6 +1,8 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { BankAccount } from "@bitwarden/common/billing/models/domain"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { BillingServicesModule } from "./billing-services.module"; import { BillingServicesModule } from "./billing-services.module";
@ -15,7 +17,10 @@ export class StripeService {
cardCvc: string; cardCvc: string;
}; };
constructor(private logService: LogService) {} constructor(
private logService: LogService,
private configService: ConfigService,
) {}
/** /**
* Loads [Stripe JS]{@link https://docs.stripe.com/js} in the <head> element of the current page and mounts * Loads [Stripe JS]{@link https://docs.stripe.com/js} in the <head> element of the current page and mounts
@ -32,15 +37,23 @@ export class StripeService {
const script = window.document.createElement("script"); const script = window.document.createElement("script");
script.id = "stripe-script"; script.id = "stripe-script";
script.src = "https://js.stripe.com/v3?advancedFraudSignals=false"; script.src = "https://js.stripe.com/v3?advancedFraudSignals=false";
script.onload = () => { script.onload = async () => {
const window$ = window as any; const window$ = window as any;
this.stripe = window$.Stripe(process.env.STRIPE_KEY); this.stripe = window$.Stripe(process.env.STRIPE_KEY);
this.elements = this.stripe.elements(); this.elements = this.stripe.elements();
const options = this.getElementOptions(); const isExtensionRefresh = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
setTimeout(() => { setTimeout(() => {
this.elements.create("cardNumber", options); this.elements.create(
this.elements.create("cardExpiry", options); "cardNumber",
this.elements.create("cardCvc", options); this.getElementOptions("cardNumber", isExtensionRefresh),
);
this.elements.create(
"cardExpiry",
this.getElementOptions("cardExpiry", isExtensionRefresh),
);
this.elements.create("cardCvc", this.getElementOptions("cardCvc", isExtensionRefresh));
if (autoMount) { if (autoMount) {
this.mountElements(); this.mountElements();
} }
@ -135,7 +148,10 @@ export class StripeService {
}, 500); }, 500);
} }
private getElementOptions(): any { private getElementOptions(
element: "cardNumber" | "cardExpiry" | "cardCvc",
isExtensionRefresh: boolean,
): any {
const options: any = { const options: any = {
style: { style: {
base: { 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); const style = getComputedStyle(document.documentElement);
options.style.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`; options.style.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
options.style.base["::placeholder"].color = `rgb(${style.getPropertyValue( options.style.base["::placeholder"].color = `rgb(${style.getPropertyValue(

View File

@ -0,0 +1,22 @@
<ng-template #defaultContent>
<ng-content></ng-content>
</ng-template>
<ng-container *ngIf="extensionRefreshFlag; else defaultLabel">
<div class="tw-relative tw-mt-2">
<bit-label
[attr.for]="for"
class="tw-absolute tw-bg-background tw-px-1 tw-text-sm tw-text-muted -tw-top-2.5 tw-left-3 tw-mb-0 tw-max-w-full tw-pointer-events-auto"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span>
</bit-label>
</div>
</ng-container>
<ng-template #defaultLabel>
<label [attr.for]="for">
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span>
</label>
</ng-template>

View File

@ -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<void> {
this.extensionRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
}

View File

@ -43,10 +43,9 @@
<ng-container *ngIf="usingCard"> <ng-container *ngIf="usingCard">
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4"> <div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4">
<div class="tw-col-span-1"> <div class="tw-col-span-1">
<label for="stripe-card-number"> <app-payment-label-v2 for="stripe-card-number" required>
{{ "number" | i18n }} {{ "number" | i18n }}
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span> </app-payment-label-v2>
</label>
<div id="stripe-card-number" class="form-control stripe-form-control"></div> <div id="stripe-card-number" class="form-control stripe-form-control"></div>
</div> </div>
<div class="tw-col-span-1 tw-flex tw-items-end"> <div class="tw-col-span-1 tw-flex tw-items-end">
@ -57,29 +56,24 @@
/> />
</div> </div>
<div class="tw-col-span-1"> <div class="tw-col-span-1">
<label for="stripe-card-expiry"> <app-payment-label-v2 for="stripe-card-expiry" required>
{{ "expiration" | i18n }} {{ "expiration" | i18n }}
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span> </app-payment-label-v2>
</label>
<div id="stripe-card-expiry" class="form-control stripe-form-control"></div> <div id="stripe-card-expiry" class="form-control stripe-form-control"></div>
</div> </div>
<div class="tw-col-span-1"> <div class="tw-col-span-1">
<div class="tw-flex"> <app-payment-label-v2 for="stripe-card-cvc" required>
<label for="stripe-card-cvc"> {{ "securityCodeSlashCVV" | i18n }}
{{ "securityCode" | i18n }}
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span>
</label>
<a <a
href="https://www.cvvnumber.com/cvv.html" href="https://www.cvvnumber.com/cvv.html"
tabindex="-1"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
class="ml-auto"
appA11yTitle="{{ 'learnMore' | i18n }}" appA11yTitle="{{ 'learnMore' | i18n }}"
class="hover:tw-no-underline"
> >
<i class="bwi bwi-question-circle" aria-hidden="true"></i> <i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
</div> </app-payment-label-v2>
<div id="stripe-card-cvc" class="form-control stripe-form-control"></div> <div id="stripe-card-cvc" class="form-control stripe-form-control"></div>
</div> </div>
</div> </div>

View File

@ -10,6 +10,8 @@ import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/
import { SharedModule } from "../../../shared"; import { SharedModule } from "../../../shared";
import { BillingServicesModule, BraintreeService, StripeService } from "../../services"; 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, * 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. * 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", selector: "app-payment-v2",
templateUrl: "./payment-v2.component.html", templateUrl: "./payment-v2.component.html",
standalone: true, standalone: true,
imports: [BillingServicesModule, SharedModule], imports: [BillingServicesModule, SharedModule, PaymentLabelV2],
}) })
export class PaymentV2Component implements OnInit, OnDestroy { export class PaymentV2Component implements OnInit, OnDestroy {
/** Show account credit as a payment option. */ /** Show account credit as a payment option. */

View File

@ -26,8 +26,10 @@
</div> </div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card"> <ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4"> <div class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4">
<div [ngClass]="trialFlow ? 'tw-col-span-5' : 'tw-col-span-4'"> <div [ngClass]="trialFlow ? 'tw-col-span-12' : 'tw-col-span-4'">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label> <app-payment-label-v2 for="stripe-card-number-element">{{
"number" | i18n
}}</app-payment-label-v2>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div> <div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div> </div>
<div *ngIf="!trialFlow" class="tw-col-span-8 tw-flex tw-items-end"> <div *ngIf="!trialFlow" class="tw-col-span-8 tw-flex tw-items-end">
@ -38,26 +40,26 @@
height="32" height="32"
/> />
</div> </div>
<div [ngClass]="trialFlow ? 'tw-col-span-3' : 'tw-col-span-4'"> <div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-4'">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label> <app-payment-label-v2 for="stripe-card-expiry-element">{{
"expiration" | i18n
}}</app-payment-label-v2>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div> <div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div> </div>
<div class="tw-col-span-4"> <div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-4'">
<div class="tw-flex"> <app-payment-label-v2 for="stripe-card-cvc-element">
<label for="stripe-card-cvc-element"> {{ "securityCodeSlashCVV" | i18n }}
{{ "securityCode" | i18n }}
</label>
<a <a
href="https://www.cvvnumber.com/cvv.html" href="https://www.cvvnumber.com/cvv.html"
tabindex="-1" tabindex="-1"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
class="ml-auto" class="hover:tw-no-underline"
appA11yTitle="{{ 'learnMore' | i18n }}" appA11yTitle="{{ 'learnMore' | i18n }}"
> >
<i class="bwi bwi-question-circle" aria-hidden="true"></i> <i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
</div> </app-payment-label-v2>
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div> <div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
</div> </div>
</div> </div>

View File

@ -5,15 +5,19 @@ import { Subject, takeUntil } from "rxjs";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PaymentMethodType } from "@bitwarden/common/billing/enums"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SharedModule } from "../../../shared"; import { SharedModule } from "../../../shared";
import { PaymentLabelV2 } from "./payment-label-v2.component";
@Component({ @Component({
selector: "app-payment", selector: "app-payment",
templateUrl: "payment.component.html", templateUrl: "payment.component.html",
standalone: true, standalone: true,
imports: [SharedModule], imports: [SharedModule, PaymentLabelV2],
}) })
export class PaymentComponent implements OnInit, OnDestroy { export class PaymentComponent implements OnInit, OnDestroy {
@Input() showMethods = true; @Input() showMethods = true;
@ -63,14 +67,15 @@ export class PaymentComponent implements OnInit, OnDestroy {
private apiService: ApiService, private apiService: ApiService,
private logService: LogService, private logService: LogService,
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
private configService: ConfigService,
) { ) {
this.stripeScript = window.document.createElement("script"); this.stripeScript = window.document.createElement("script");
this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false"; this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false";
this.stripeScript.async = true; this.stripeScript.async = true;
this.stripeScript.onload = () => { this.stripeScript.onload = async () => {
this.stripe = (window as any).Stripe(process.env.STRIPE_KEY); this.stripe = (window as any).Stripe(process.env.STRIPE_KEY);
this.stripeElements = this.stripe.elements(); this.stripeElements = this.stripe.elements();
this.setStripeElement(); await this.setStripeElement();
}; };
this.btScript = window.document.createElement("script"); this.btScript = window.document.createElement("script");
this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`; this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
@ -187,7 +192,7 @@ export class PaymentComponent implements OnInit, OnDestroy {
); );
}, 250); }, 250);
} else { } 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(() => { window.setTimeout(() => {
if (this.showMethods && this.method === PaymentMethodType.Card) { if (this.showMethods && this.method === PaymentMethodType.Card) {
if (this.stripeCardNumberElement == null) { if (this.stripeCardNumberElement == null) {

View File

@ -1,6 +1,6 @@
<form [formGroup]="taxFormGroup"> <form [formGroup]="taxFormGroup">
<div class="tw-grid tw-grid-cols-12 tw-gap-4"> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div [ngClass]="trialFlow ? 'tw-col-span-7' : 'tw-col-span-6'"> <div class="tw-col-span-6">
<bit-form-field> <bit-form-field>
<bit-label>{{ "country" | i18n }}</bit-label> <bit-label>{{ "country" | i18n }}</bit-label>
<bit-select formControlName="country" autocomplete="country"> <bit-select formControlName="country" autocomplete="country">
@ -13,7 +13,7 @@
</bit-select> </bit-select>
</bit-form-field> </bit-form-field>
</div> </div>
<div [ngClass]="trialFlow ? 'tw-col-span-5' : 'tw-col-span-4'"> <div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-4'">
<bit-form-field> <bit-form-field>
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label> <bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" /> <input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />

View File

@ -143,6 +143,9 @@
"securityCode": { "securityCode": {
"message": "Security code (CVV)" "message": "Security code (CVV)"
}, },
"securityCodeSlashCVV": {
"message": "Security code / CVV"
},
"identityName": { "identityName": {
"message": "Identity name" "message": "Identity name"
}, },

View File

@ -98,7 +98,7 @@ input[type="checkbox"] {
cursor: pointer; cursor: pointer;
} }
.form-control.stripe-form-control { .form-control.stripe-form-control:not(.v2) {
padding-top: 0.55rem; padding-top: 0.55rem;
&.is-focused { &.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-menu,
.dropdown-item { .dropdown-item {
@include themify($themes) { @include themify($themes) {