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 { 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 <head> 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(

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

View File

@ -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. */

View File

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

View File

@ -1,6 +1,6 @@
<form [formGroup]="taxFormGroup">
<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-label>{{ "country" | i18n }}</bit-label>
<bit-select formControlName="country" autocomplete="country">
@ -13,7 +13,7 @@
</bit-select>
</bit-form-field>
</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-label>{{ "zipPostalCode" | i18n }}</bit-label>
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />

View File

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

View File

@ -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) {