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:
parent
b6cde7e3ef
commit
70fbcf2a10
@ -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(
|
||||
|
@ -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>
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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. */
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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" />
|
||||
|
@ -143,6 +143,9 @@
|
||||
"securityCode": {
|
||||
"message": "Security code (CVV)"
|
||||
},
|
||||
"securityCodeSlashCVV": {
|
||||
"message": "Security code / CVV"
|
||||
},
|
||||
"identityName": {
|
||||
"message": "Identity name"
|
||||
},
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user