[PM-1823] Defining the card brand according to its number (#5204)

* Defining the card brand according to its number

* Moving cardBrandByPatterns function to Card View

* Getting Card brand via cardBrandByPatterns function

* Changing cardBrandByPatterns method to static. See:
The reason being that someone wanting to use this outside of the onCardNumberChange would need to know to set the cc-number on the view-model before calling cardBrandByPatterns

* Defining the card brand according to its number on Desktop

* Defining the card brand according to its number on Web
This commit is contained in:
Thales Augusto 2023-06-09 15:44:33 -03:00 committed by GitHub
parent ab260a3653
commit c70d67bad3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 89 additions and 79 deletions

View File

@ -163,6 +163,7 @@
class="monospaced"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
(input)="onCardNumberChange()"
[(ngModel)]="cipher.card.number"
appInputVerbatim
[readonly]="!cipher.edit && editMode"

View File

@ -148,6 +148,7 @@
class="monospaced"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
(input)="onCardNumberChange()"
[(ngModel)]="cipher.card.number"
appInputVerbatim
[readonly]="!cipher.edit && editMode"

View File

@ -440,6 +440,7 @@
class="form-control text-monospace"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
(input)="onCardNumberChange()"
[(ngModel)]="cipher.card.number"
appInputVerbatim
autocomplete="new-password"

View File

@ -368,6 +368,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
onCardNumberChange(): void {
this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number);
}
getCardExpMonthDisplay() {
return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name;
}

View File

@ -81,4 +81,67 @@ export class CardView extends ItemView {
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
return Object.assign(new CardView(), obj);
}
// ref https://stackoverflow.com/a/5911300
static getCardBrandByPatterns(cardNum: string): string {
if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") {
return null;
}
// Visa
let re = new RegExp("^4");
if (cardNum.match(re) != null) {
return "Visa";
}
// Mastercard
// Updated for Mastercard 2017 BINs expansion
if (
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
cardNum
)
) {
return "Mastercard";
}
// AMEX
re = new RegExp("^3[47]");
if (cardNum.match(re) != null) {
return "Amex";
}
// Discover
re = new RegExp(
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
);
if (cardNum.match(re) != null) {
return "Discover";
}
// Diners
re = new RegExp("^36");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// Diners - Carte Blanche
re = new RegExp("^30[0-5]");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// JCB
re = new RegExp("^35(2[89]|[3-8][0-9])");
if (cardNum.match(re) != null) {
return "JCB";
}
// Visa Electron
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
if (cardNum.match(re) != null) {
return "Visa";
}
return null;
}
}

View File

@ -1,5 +1,6 @@
import { SecureNoteType } from "@bitwarden/common/enums";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { ImportResult } from "../../models/import-result";
import { BaseImporter } from "../base-importer";
@ -48,7 +49,7 @@ export class AvastJsonImporter extends BaseImporter implements Importer {
cipher.card.cardholderName = this.getValueOrDefault(value.holderName);
cipher.card.number = this.getValueOrDefault(value.cardNumber);
cipher.card.code = this.getValueOrDefault(value.cvv);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
if (value.expirationDate != null) {
if (value.expirationDate.month != null) {
cipher.card.expMonth = value.expirationDate.month + "";

View File

@ -241,69 +241,6 @@ export abstract class BaseImporter {
return str.split(this.newLineRegex);
}
// ref https://stackoverflow.com/a/5911300
protected getCardBrand(cardNum: string) {
if (this.isNullOrWhitespace(cardNum)) {
return null;
}
// Visa
let re = new RegExp("^4");
if (cardNum.match(re) != null) {
return "Visa";
}
// Mastercard
// Updated for Mastercard 2017 BINs expansion
if (
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test(
cardNum
)
) {
return "Mastercard";
}
// AMEX
re = new RegExp("^3[47]");
if (cardNum.match(re) != null) {
return "Amex";
}
// Discover
re = new RegExp(
"^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)"
);
if (cardNum.match(re) != null) {
return "Discover";
}
// Diners
re = new RegExp("^36");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// Diners - Carte Blanche
re = new RegExp("^30[0-5]");
if (cardNum.match(re) != null) {
return "Diners Club";
}
// JCB
re = new RegExp("^35(2[89]|[3-8][0-9])");
if (cardNum.match(re) != null) {
return "JCB";
}
// Visa Electron
re = new RegExp("^(4026|417500|4508|4844|491(3|7))");
if (cardNum.match(re) != null) {
return "Visa";
}
return null;
}
protected setCardExpiration(cipher: CipherView, expiration: string): boolean {
if (this.isNullOrWhitespace(expiration)) {
return false;

View File

@ -136,7 +136,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer {
case "credit_card":
cipher.card.cardholderName = row.account_name;
cipher.card.number = row.cc_number;
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.code = row.code;
this.setCardExpiration(cipher, `${row.expiration_month}/${row.expiration_year}`);

View File

@ -134,7 +134,7 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer {
cipher.type = CipherType.Card;
cipher.name = this.getValueOrDefault(obj.bank);
cipher.card.number = this.getValueOrDefault(obj.cardNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.cardholderName = this.getValueOrDefault(obj.owner);
if (!this.isNullOrWhitespace(cipher.card.brand)) {
if (this.isNullOrWhitespace(cipher.name)) {

View File

@ -38,7 +38,7 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer {
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(value["Name on card"]);
cipher.card.number = this.getValueOrDefault(value["Card Number"]);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.code = this.getValueOrDefault(value.CVV);
const expiry = this.getValueOrDefault(value.Expiry);
if (!this.isNullOrWhitespace(expiry)) {

View File

@ -90,7 +90,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer {
continue;
} else if (fieldNameLower === "number" && this.isNullOrWhitespace(cipher.card.number)) {
cipher.card.number = fieldValue;
cipher.card.brand = this.getCardBrand(fieldValue);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
continue;
} else if (fieldNameLower === "cvc" && this.isNullOrWhitespace(cipher.card.code)) {
cipher.card.code = fieldValue;

View File

@ -126,7 +126,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
cipher.card.cardholderName = field.value;
} else if (field.type === "ccNumber" && this.isNullOrWhitespace(cipher.card.number)) {
cipher.card.number = field.value;
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
} else if (field.type === "ccCvc" && this.isNullOrWhitespace(cipher.card.code)) {
cipher.card.code = field.value;
} else if (field.type === "ccExpiry" && this.isNullOrWhitespace(cipher.card.expYear)) {

View File

@ -66,7 +66,7 @@ export class FSecureFskImporter extends BaseImporter implements Importer {
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(entry.username);
cipher.card.number = this.getValueOrDefault(entry.creditNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.code = this.getValueOrDefault(entry.creditCvv);
if (!this.isNullOrWhitespace(entry.creditExpiry)) {
if (!this.setCardExpiration(cipher, entry.creditExpiry)) {

View File

@ -122,7 +122,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer {
card.cardholderName = this.getValueOrDefault(value.ccname);
card.number = this.getValueOrDefault(value.ccnum);
card.code = this.getValueOrDefault(value.cccsc);
card.brand = this.getCardBrand(value.ccnum);
card.brand = CardView.getCardBrandByPatterns(card.number);
if (!this.isNullOrWhitespace(value.ccexp) && value.ccexp.indexOf("-") > -1) {
const ccexpParts = (value.ccexp as string).split("-");

View File

@ -72,7 +72,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer {
cipher.type = CipherType.Card;
cipher.card.cardholderName = this.getValueOrDefault(value.cardName);
cipher.card.number = this.getValueOrDefault(value.cardNumber);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.expMonth = this.getValueOrDefault(value.exp_month);
cipher.card.expYear = this.getValueOrDefault(value.exp_year);
cipher.card.code = this.getValueOrDefault(value.cvv);

View File

@ -1,5 +1,6 @@
import { SecureNoteType } from "@bitwarden/common/enums";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
@ -66,7 +67,7 @@ export class NordPassCsvImporter extends BaseImporter implements Importer {
cipher.card.cardholderName = this.getValueOrDefault(record.cardholdername);
cipher.card.number = this.getValueOrDefault(record.cardnumber);
cipher.card.code = this.getValueOrDefault(record.cvc);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
this.setCardExpiration(cipher, record.expirydate);
break;

View File

@ -198,7 +198,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer {
} else if (cipher.type === CipherType.Card) {
if (this.isNullOrWhitespace(cipher.card.number) && fieldDesignation === "ccnum") {
cipher.card.number = fieldValue;
cipher.card.brand = this.getCardBrand(fieldValue);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
return;
} else if (this.isNullOrWhitespace(cipher.card.code) && fieldDesignation === "cvv") {
cipher.card.code = fieldValue;

View File

@ -407,7 +407,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer {
private fillCreditCard(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean {
if (this.isNullOrWhitespace(cipher.card.number) && field.id === "ccnum") {
cipher.card.number = fieldValue;
cipher.card.brand = this.getCardBrand(fieldValue);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
return true;
}

View File

@ -1,5 +1,6 @@
import { FieldType } from "@bitwarden/common/enums";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ImportResult } from "../../models/import-result";
@ -295,7 +296,7 @@ export abstract class OnePasswordCsvImporter extends BaseImporter implements Imp
context.lowerProperty.includes("number")
) {
context.cipher.card.number = context.importRecord[context.property];
context.cipher.card.brand = this.getCardBrand(context.cipher.card.number);
context.cipher.card.brand = CardView.getCardBrandByPatterns(context.cipher.card.number);
return true;
}
return false;

View File

@ -75,7 +75,7 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer {
if (cipher.type === CipherType.Card) {
if (property === "cardNumber") {
cipher.card.number = val;
cipher.card.brand = this.getCardBrand(val);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
continue;
} else if (property === "nameOnCard") {
cipher.card.cardholderName = val;

View File

@ -31,7 +31,7 @@ export class RememBearCsvImporter extends BaseImporter implements Importer {
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(value.cardholder);
cipher.card.number = this.getValueOrDefault(value.number);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
cipher.card.code = this.getValueOrDefault(value.verification);
try {

View File

@ -50,7 +50,7 @@ export class TrueKeyCsvImporter extends BaseImporter implements Importer {
cipher.card = new CardView();
cipher.card.cardholderName = this.getValueOrDefault(value.cardholder);
cipher.card.number = this.getValueOrDefault(value.number);
cipher.card.brand = this.getCardBrand(cipher.card.number);
cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number);
if (!this.isNullOrWhitespace(value.expiryDate)) {
try {
const expDate = new Date(value.expiryDate);