mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01:00
[PM-10418] Bugfix - Expiration date on cards does not always autofill the correct format (#10705)
* add branching logic for alternative card expiration autofill strategy * simplify logic and fix some pattern-matching bugs * add EnableNewCardCombinedExpiryAutofill feature flag * update default format for card expiry date and update tests * review reccs
This commit is contained in:
parent
56ededa947
commit
9881c7842b
@ -90,6 +90,7 @@ export class CreditCardAutoFillConstants {
|
|||||||
"data-stripe",
|
"data-stripe",
|
||||||
"htmlName",
|
"htmlName",
|
||||||
"htmlID",
|
"htmlID",
|
||||||
|
"title",
|
||||||
"label-tag",
|
"label-tag",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
"label-left",
|
"label-left",
|
||||||
@ -299,6 +300,54 @@ export class CreditCardAutoFillConstants {
|
|||||||
"cb-type",
|
"cb-type",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static readonly CardExpiryDateDelimiters: string[] = ["/", "-", ".", " "];
|
||||||
|
|
||||||
|
// Note, these are expressions of user-guidance for the expected expiry date format to be used
|
||||||
|
static readonly CardExpiryDateFormats: CardExpiryDateFormat[] = [
|
||||||
|
// English
|
||||||
|
{
|
||||||
|
Month: "mm",
|
||||||
|
MonthShort: "m",
|
||||||
|
Year: "yyyy",
|
||||||
|
YearShort: "yy",
|
||||||
|
},
|
||||||
|
// Danish
|
||||||
|
{
|
||||||
|
Month: "mm",
|
||||||
|
MonthShort: "m",
|
||||||
|
Year: "åååå",
|
||||||
|
YearShort: "åå",
|
||||||
|
},
|
||||||
|
// German/Dutch
|
||||||
|
{
|
||||||
|
Month: "mm",
|
||||||
|
MonthShort: "m",
|
||||||
|
Year: "jjjj",
|
||||||
|
YearShort: "jj",
|
||||||
|
},
|
||||||
|
// French/Spanish/Italian
|
||||||
|
{
|
||||||
|
Month: "mm",
|
||||||
|
MonthShort: "m",
|
||||||
|
Year: "aa",
|
||||||
|
YearShort: "aa",
|
||||||
|
},
|
||||||
|
// Russian
|
||||||
|
{
|
||||||
|
Month: "мм",
|
||||||
|
MonthShort: "м",
|
||||||
|
Year: "гггг",
|
||||||
|
YearShort: "гг",
|
||||||
|
},
|
||||||
|
// Portuguese
|
||||||
|
{
|
||||||
|
Month: "mm",
|
||||||
|
MonthShort: "m",
|
||||||
|
Year: "rrrr",
|
||||||
|
YearShort: "rr",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Each index represents a language. These three arrays should all be the same length.
|
// Each index represents a language. These three arrays should all be the same length.
|
||||||
// 0: English, 1: Danish, 2: German/Dutch, 3: French/Spanish/Italian, 4: Russian, 5: Portuguese
|
// 0: English, 1: Danish, 2: German/Dutch, 3: French/Spanish/Italian, 4: Russian, 5: Portuguese
|
||||||
static readonly MonthAbbr = ["mm", "mm", "mm", "mm", "мм", "mm"];
|
static readonly MonthAbbr = ["mm", "mm", "mm", "mm", "мм", "mm"];
|
||||||
@ -306,6 +355,13 @@ export class CreditCardAutoFillConstants {
|
|||||||
static readonly YearAbbrLong = ["yyyy", "åååå", "jjjj", "aa", "гггг", "rrrr"];
|
static readonly YearAbbrLong = ["yyyy", "åååå", "jjjj", "aa", "гггг", "rrrr"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CardExpiryDateFormat = {
|
||||||
|
Month: string;
|
||||||
|
MonthShort: string;
|
||||||
|
Year: string;
|
||||||
|
YearShort: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class IdentityAutoFillConstants {
|
export class IdentityAutoFillConstants {
|
||||||
static readonly IdentityAttributes: string[] = [
|
static readonly IdentityAttributes: string[] = [
|
||||||
"autoCompleteType",
|
"autoCompleteType",
|
||||||
|
@ -2475,10 +2475,10 @@ describe("AutofillService", () => {
|
|||||||
options.cipher.card = mock<CardView>();
|
options.cipher.card = mock<CardView>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null if the passed options contains a cipher with no card view", () => {
|
it("returns null if the passed options contains a cipher with no card view", async () => {
|
||||||
options.cipher.card = undefined;
|
options.cipher.card = undefined;
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2499,7 +2499,7 @@ describe("AutofillService", () => {
|
|||||||
untrustedIframe: false,
|
untrustedIframe: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("returns an unmodified fill script when the field is a `span` field", () => {
|
it("returns an unmodified fill script when the field is a `span` field", async () => {
|
||||||
const spanField = createAutofillFieldMock({
|
const spanField = createAutofillFieldMock({
|
||||||
opid: "span-field",
|
opid: "span-field",
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
@ -2510,7 +2510,7 @@ describe("AutofillService", () => {
|
|||||||
pageDetails.fields = [spanField];
|
pageDetails.fields = [spanField];
|
||||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2522,7 +2522,7 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
AutoFillConstants.ExcludedAutofillTypes.forEach((excludedType) => {
|
AutoFillConstants.ExcludedAutofillTypes.forEach((excludedType) => {
|
||||||
it(`returns an unmodified fill script when the field has a '${excludedType}' type`, () => {
|
it(`returns an unmodified fill script when the field has a '${excludedType}' type`, async () => {
|
||||||
const invalidField = createAutofillFieldMock({
|
const invalidField = createAutofillFieldMock({
|
||||||
opid: `${excludedType}-field`,
|
opid: `${excludedType}-field`,
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
@ -2533,7 +2533,7 @@ describe("AutofillService", () => {
|
|||||||
pageDetails.fields = [invalidField];
|
pageDetails.fields = [invalidField];
|
||||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2548,7 +2548,7 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an unmodified fill script when the field is not viewable", () => {
|
it("returns an unmodified fill script when the field is not viewable", async () => {
|
||||||
const notViewableField = createAutofillFieldMock({
|
const notViewableField = createAutofillFieldMock({
|
||||||
opid: "invalid-field",
|
opid: "invalid-field",
|
||||||
form: "validFormId",
|
form: "validFormId",
|
||||||
@ -2561,7 +2561,7 @@ describe("AutofillService", () => {
|
|||||||
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
||||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2663,8 +2663,8 @@ describe("AutofillService", () => {
|
|||||||
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a fill script containing all of the passed card fields", () => {
|
it("returns a fill script containing all of the passed card fields", async () => {
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2745,11 +2745,11 @@ describe("AutofillService", () => {
|
|||||||
options.cipher.card.expMonth = "05";
|
options.cipher.card.expMonth = "05";
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration month parsed from found select options within the field", () => {
|
it("returns an expiration month parsed from found select options within the field", async () => {
|
||||||
const testValue = "sometestvalue";
|
const testValue = "sometestvalue";
|
||||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2759,12 +2759,12 @@ describe("AutofillService", () => {
|
|||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the end of the list of options", () => {
|
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the end of the list of options", async () => {
|
||||||
const testValue = "sometestvalue";
|
const testValue = "sometestvalue";
|
||||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||||
expMonthField.selectInfo.options.push(["", ""]);
|
expMonthField.selectInfo.options.push(["", ""]);
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2774,12 +2774,12 @@ describe("AutofillService", () => {
|
|||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the start of the list of options", () => {
|
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the start of the list of options", async () => {
|
||||||
const testValue = "sometestvalue";
|
const testValue = "sometestvalue";
|
||||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||||
expMonthField.selectInfo.options.unshift(["", ""]);
|
expMonthField.selectInfo.options.unshift(["", ""]);
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2789,13 +2789,13 @@ describe("AutofillService", () => {
|
|||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration month with a zero attached if the field requires two characters, and the vault item has only one character", () => {
|
it("returns an expiration month with a zero attached if the field requires two characters, and the vault item has only one character", async () => {
|
||||||
options.cipher.card.expMonth = "5";
|
options.cipher.card.expMonth = "5";
|
||||||
expMonthField.selectInfo = null;
|
expMonthField.selectInfo = null;
|
||||||
expMonthField.placeholder = "mm";
|
expMonthField.placeholder = "mm";
|
||||||
expMonthField.maxLength = 2;
|
expMonthField.maxLength = 2;
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2830,12 +2830,12 @@ describe("AutofillService", () => {
|
|||||||
options.cipher.card.expYear = "2024";
|
options.cipher.card.expYear = "2024";
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration year parsed from the select options if an exact match is found for either the select option text or value", () => {
|
it("returns an expiration year parsed from the select options if an exact match is found for either the select option text or value", async () => {
|
||||||
const someTestValue = "sometestvalue";
|
const someTestValue = "sometestvalue";
|
||||||
expYearField.selectInfo.options[1] = ["2024", someTestValue];
|
expYearField.selectInfo.options[1] = ["2024", someTestValue];
|
||||||
options.cipher.card.expYear = someTestValue;
|
options.cipher.card.expYear = someTestValue;
|
||||||
|
|
||||||
let value = autofillService["generateCardFillScript"](
|
let value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2846,7 +2846,7 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
expYearField.selectInfo.options[1] = [someTestValue, "2024"];
|
expYearField.selectInfo.options[1] = [someTestValue, "2024"];
|
||||||
|
|
||||||
value = autofillService["generateCardFillScript"](
|
value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2856,12 +2856,12 @@ describe("AutofillService", () => {
|
|||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, someTestValue]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, someTestValue]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration year parsed from the select options if the value of an option contains only two characters and the vault item value contains four characters", () => {
|
it("returns an expiration year parsed from the select options if the value of an option contains only two characters and the vault item value contains four characters", async () => {
|
||||||
const yearValue = "26";
|
const yearValue = "26";
|
||||||
expYearField.selectInfo.options.push(["The year 2026", yearValue]);
|
expYearField.selectInfo.options.push(["The year 2026", yearValue]);
|
||||||
options.cipher.card.expYear = "2026";
|
options.cipher.card.expYear = "2026";
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2871,13 +2871,13 @@ describe("AutofillService", () => {
|
|||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, yearValue]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, yearValue]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration year parsed from the select options if the vault of an option is separated by a colon", () => {
|
it("returns an expiration year parsed from the select options if the vault of an option is separated by a colon", async () => {
|
||||||
const yearValue = "26";
|
const yearValue = "26";
|
||||||
const colonSeparatedYearValue = `2:0${yearValue}`;
|
const colonSeparatedYearValue = `2:0${yearValue}`;
|
||||||
expYearField.selectInfo.options.push(["The year 2026", colonSeparatedYearValue]);
|
expYearField.selectInfo.options.push(["The year 2026", colonSeparatedYearValue]);
|
||||||
options.cipher.card.expYear = yearValue;
|
options.cipher.card.expYear = yearValue;
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2891,14 +2891,14 @@ describe("AutofillService", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration year with `20` prepended to the vault item value if the field to be filled expects a `yyyy` format but the vault item only has two characters", () => {
|
it("returns an expiration year with `20` prepended to the vault item value if the field to be filled expects a `yyyy` format but the vault item only has two characters", async () => {
|
||||||
const yearValue = "26";
|
const yearValue = "26";
|
||||||
expYearField.selectInfo = null;
|
expYearField.selectInfo = null;
|
||||||
expYearField.placeholder = "yyyy";
|
expYearField.placeholder = "yyyy";
|
||||||
expYearField.maxLength = 4;
|
expYearField.maxLength = 4;
|
||||||
options.cipher.card.expYear = yearValue;
|
options.cipher.card.expYear = yearValue;
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2912,14 +2912,14 @@ describe("AutofillService", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration year with only the last two values if the field to be filled expects a `yy` format but the vault item contains four characters", () => {
|
it("returns an expiration year with only the last two values if the field to be filled expects a `yy` format but the vault item contains four characters", async () => {
|
||||||
const yearValue = "26";
|
const yearValue = "26";
|
||||||
expYearField.selectInfo = null;
|
expYearField.selectInfo = null;
|
||||||
expYearField.placeholder = "yy";
|
expYearField.placeholder = "yy";
|
||||||
expYearField.maxLength = 2;
|
expYearField.maxLength = 2;
|
||||||
options.cipher.card.expYear = `20${yearValue}`;
|
options.cipher.card.expYear = `20${yearValue}`;
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2930,25 +2930,6 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("given a generic expiration date field", () => {
|
|
||||||
let expirationDateField: AutofillField;
|
|
||||||
let expirationDateFieldView: FieldView;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
expirationDateField = createAutofillFieldMock({
|
|
||||||
opid: "expirationDate",
|
|
||||||
form: "validFormId",
|
|
||||||
elementNumber: 3,
|
|
||||||
htmlName: "expiration-date",
|
|
||||||
});
|
|
||||||
filledFields["exp-field"] = expirationDateField;
|
|
||||||
expirationDateFieldView = mock<FieldView>({ name: "exp" });
|
|
||||||
pageDetails.fields = [expirationDateField];
|
|
||||||
options.cipher.fields = [expirationDateFieldView];
|
|
||||||
options.cipher.card.expMonth = "05";
|
|
||||||
options.cipher.card.expYear = "2024";
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedDateFormats = [
|
const expectedDateFormats = [
|
||||||
["mm/yyyy", "05/2024"],
|
["mm/yyyy", "05/2024"],
|
||||||
["mm/yy", "05/24"],
|
["mm/yy", "05/24"],
|
||||||
@ -2963,9 +2944,31 @@ describe("AutofillService", () => {
|
|||||||
["mmyyyy", "052024"],
|
["mmyyyy", "052024"],
|
||||||
["mmyy", "0524"],
|
["mmyy", "0524"],
|
||||||
];
|
];
|
||||||
|
describe("given a generic expiration date field", () => {
|
||||||
|
let expirationDateField: AutofillField;
|
||||||
|
let expirationDateFieldView: FieldView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(false);
|
||||||
|
expirationDateField = createAutofillFieldMock({
|
||||||
|
opid: "expirationDate",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 3,
|
||||||
|
htmlName: "expiration-date",
|
||||||
|
});
|
||||||
|
filledFields["exp-field"] = expirationDateField;
|
||||||
|
expirationDateFieldView = mock<FieldView>({ name: "exp" });
|
||||||
|
pageDetails.fields = [expirationDateField];
|
||||||
|
options.cipher.fields = [expirationDateFieldView];
|
||||||
|
options.cipher.card.expMonth = "05";
|
||||||
|
options.cipher.card.expYear = "2024";
|
||||||
|
});
|
||||||
|
|
||||||
expectedDateFormats.forEach((dateFormat, index) => {
|
expectedDateFormats.forEach((dateFormat, index) => {
|
||||||
it(`returns an expiration date format matching '${dateFormat[0]}'`, () => {
|
it(`returns an expiration date format matching '${dateFormat[0]}'`, async () => {
|
||||||
expirationDateField.placeholder = dateFormat[0];
|
expirationDateField.placeholder = dateFormat[0];
|
||||||
|
|
||||||
|
// test alternate stored cipher value formats
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
options.cipher.card.expYear = "24";
|
options.cipher.card.expYear = "24";
|
||||||
}
|
}
|
||||||
@ -2973,7 +2976,13 @@ describe("AutofillService", () => {
|
|||||||
options.cipher.card.expMonth = "5";
|
options.cipher.card.expMonth = "5";
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = autofillService["generateCardFillScript"](
|
const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enableNewCardCombinedExpiryAutofill).toEqual(false);
|
||||||
|
|
||||||
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
@ -2984,17 +2993,128 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", () => {
|
it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", async () => {
|
||||||
const value = autofillService["generateCardFillScript"](
|
const value = await autofillService["generateCardFillScript"](
|
||||||
fillScript,
|
fillScript,
|
||||||
pageDetails,
|
pageDetails,
|
||||||
filledFields,
|
filledFields,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enableNewCardCombinedExpiryAutofill).toEqual(false);
|
||||||
|
|
||||||
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]);
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extraExpectedDateFormats = [
|
||||||
|
...expectedDateFormats,
|
||||||
|
["m yy", "5 24"],
|
||||||
|
["m yyyy", "5 2024"],
|
||||||
|
["m-yy", "5-24"],
|
||||||
|
["m-yyyy", "5-2024"],
|
||||||
|
["m.yy", "5.24"],
|
||||||
|
["m.yyyy", "5.2024"],
|
||||||
|
["m/yy", "5/24"],
|
||||||
|
["m/yyyy", "5/2024"],
|
||||||
|
["mm åååå", "05 2024"],
|
||||||
|
["mm yy", "05 24"],
|
||||||
|
["mm yyyy", "05 2024"],
|
||||||
|
["mm.yy", "05.24"],
|
||||||
|
["mm.yyyy", "05.2024"],
|
||||||
|
["myy", "524"],
|
||||||
|
["myyyy", "52024"],
|
||||||
|
["yy m", "24 5"],
|
||||||
|
["yy mm", "24 05"],
|
||||||
|
["yy mm", "24 05"],
|
||||||
|
["yy-m", "24-5"],
|
||||||
|
["yy.m", "24.5"],
|
||||||
|
["yy.mm", "24.05"],
|
||||||
|
["yy/m", "24/5"],
|
||||||
|
["yym", "245"],
|
||||||
|
["yyyy m", "2024 5"],
|
||||||
|
["yyyy mm", "2024 05"],
|
||||||
|
["yyyy-m", "2024-5"],
|
||||||
|
["yyyy.m", "2024.5"],
|
||||||
|
["yyyy.mm", "2024.05"],
|
||||||
|
["yyyy/m", "2024/5"],
|
||||||
|
["yyyym", "20245"],
|
||||||
|
["мм гг", "05 24"],
|
||||||
|
];
|
||||||
|
describe("given a generic expiration date field with the `enable-new-card-combined-expiry-autofill` feature-flag enabled", () => {
|
||||||
|
let expirationDateField: AutofillField;
|
||||||
|
let expirationDateFieldView: FieldView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(true);
|
||||||
|
expirationDateField = createAutofillFieldMock({
|
||||||
|
opid: "expirationDate",
|
||||||
|
form: "validFormId",
|
||||||
|
elementNumber: 3,
|
||||||
|
htmlName: "expiration-date",
|
||||||
|
});
|
||||||
|
filledFields["exp-field"] = expirationDateField;
|
||||||
|
expirationDateFieldView = mock<FieldView>({ name: "exp" });
|
||||||
|
pageDetails.fields = [expirationDateField];
|
||||||
|
options.cipher.fields = [expirationDateFieldView];
|
||||||
|
options.cipher.card.expMonth = "05";
|
||||||
|
options.cipher.card.expYear = "2024";
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
configService.getFeatureFlag.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
extraExpectedDateFormats.forEach((dateFormat, index) => {
|
||||||
|
it(`feature-flagged logic returns an expiration date format matching '${dateFormat[0]}'`, async () => {
|
||||||
|
expirationDateField.placeholder = dateFormat[0];
|
||||||
|
|
||||||
|
// test alternate stored cipher value formats
|
||||||
|
if (index === 0) {
|
||||||
|
options.cipher.card.expYear = "24";
|
||||||
|
}
|
||||||
|
if (index === 1) {
|
||||||
|
options.cipher.card.expMonth = "05";
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enableNewCardCombinedExpiryAutofill).toEqual(true);
|
||||||
|
|
||||||
|
const value = await autofillService["generateCardFillScript"](
|
||||||
|
fillScript,
|
||||||
|
pageDetails,
|
||||||
|
filledFields,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("feature-flagged logic returns an expiration date format matching `mm/yy` if no valid format can be identified", async () => {
|
||||||
|
const value = await autofillService["generateCardFillScript"](
|
||||||
|
fillScript,
|
||||||
|
pageDetails,
|
||||||
|
filledFields,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enableNewCardCombinedExpiryAutofill).toEqual(true);
|
||||||
|
|
||||||
|
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "05/24"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inUntrustedIframe", () => {
|
describe("inUntrustedIframe", () => {
|
||||||
|
@ -26,6 +26,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||||
@ -50,6 +51,7 @@ import {
|
|||||||
} from "./abstractions/autofill.service";
|
} from "./abstractions/autofill.service";
|
||||||
import {
|
import {
|
||||||
AutoFillConstants,
|
AutoFillConstants,
|
||||||
|
CardExpiryDateFormat,
|
||||||
CreditCardAutoFillConstants,
|
CreditCardAutoFillConstants,
|
||||||
IdentityAutoFillConstants,
|
IdentityAutoFillConstants,
|
||||||
} from "./autofill-constants";
|
} from "./autofill-constants";
|
||||||
@ -721,7 +723,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case CipherType.Card:
|
case CipherType.Card:
|
||||||
fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options);
|
fillScript = await this.generateCardFillScript(
|
||||||
|
fillScript,
|
||||||
|
pageDetails,
|
||||||
|
filledFields,
|
||||||
|
options,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case CipherType.Identity:
|
case CipherType.Identity:
|
||||||
fillScript = await this.generateIdentityFillScript(
|
fillScript = await this.generateIdentityFillScript(
|
||||||
@ -937,12 +944,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
* @returns {AutofillScript|null}
|
* @returns {AutofillScript|null}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private generateCardFillScript(
|
private async generateCardFillScript(
|
||||||
fillScript: AutofillScript,
|
fillScript: AutofillScript,
|
||||||
pageDetails: AutofillPageDetails,
|
pageDetails: AutofillPageDetails,
|
||||||
filledFields: { [id: string]: AutofillField },
|
filledFields: { [id: string]: AutofillField },
|
||||||
options: GenerateFillScriptOptions,
|
options: GenerateFillScriptOptions,
|
||||||
): AutofillScript | null {
|
): Promise<AutofillScript | null> {
|
||||||
if (!options.cipher.card) {
|
if (!options.cipher.card) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1027,6 +1034,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
this.makeScriptAction(fillScript, card, fillFields, filledFields, "code");
|
this.makeScriptAction(fillScript, card, fillFields, filledFields, "code");
|
||||||
this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand");
|
this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand");
|
||||||
|
|
||||||
|
// There is an expiration month field and the cipher has an expiration month value
|
||||||
if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
|
if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
|
||||||
let expMonth: string = card.expMonth;
|
let expMonth: string = card.expMonth;
|
||||||
|
|
||||||
@ -1065,6 +1073,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth);
|
AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There is an expiration year field and the cipher has an expiration year value
|
||||||
if (fillFields.expYear && AutofillService.hasValue(card.expYear)) {
|
if (fillFields.expYear && AutofillService.hasValue(card.expYear)) {
|
||||||
let expYear: string = card.expYear;
|
let expYear: string = card.expYear;
|
||||||
if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) {
|
if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) {
|
||||||
@ -1111,11 +1120,21 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear);
|
AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There is a single expiry date field (combined values) and the cipher has both expiration month and year
|
||||||
if (
|
if (
|
||||||
fillFields.exp &&
|
fillFields.exp &&
|
||||||
AutofillService.hasValue(card.expMonth) &&
|
AutofillService.hasValue(card.expMonth) &&
|
||||||
AutofillService.hasValue(card.expYear)
|
AutofillService.hasValue(card.expYear)
|
||||||
) {
|
) {
|
||||||
|
let combinedExpiryFillValue = null;
|
||||||
|
|
||||||
|
const enableNewCardCombinedExpiryAutofill = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enableNewCardCombinedExpiryAutofill) {
|
||||||
|
combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp);
|
||||||
|
} else {
|
||||||
const fullMonth = ("0" + card.expMonth).slice(-2);
|
const fullMonth = ("0" + card.expMonth).slice(-2);
|
||||||
|
|
||||||
let fullYear: string = card.expYear;
|
let fullYear: string = card.expYear;
|
||||||
@ -1127,9 +1146,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
partYear = fullYear.substr(2, 2);
|
partYear = fullYear.substr(2, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
let exp: string = null;
|
|
||||||
for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) {
|
for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) {
|
||||||
if (
|
if (
|
||||||
|
// mm/yyyy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
@ -1137,8 +1156,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + "/" + fullYear;
|
combinedExpiryFillValue = fullMonth + "/" + fullYear;
|
||||||
} else if (
|
} else if (
|
||||||
|
// mm/yy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
@ -1147,8 +1167,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + "/" + partYear;
|
combinedExpiryFillValue = fullMonth + "/" + partYear;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yyyy/mm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||||
@ -1156,8 +1177,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullYear + "/" + fullMonth;
|
combinedExpiryFillValue = fullYear + "/" + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yy/mm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||||
@ -1166,8 +1188,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = partYear + "/" + fullMonth;
|
combinedExpiryFillValue = partYear + "/" + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// mm-yyyy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
@ -1175,8 +1198,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + "-" + fullYear;
|
combinedExpiryFillValue = fullMonth + "-" + fullYear;
|
||||||
} else if (
|
} else if (
|
||||||
|
// mm-yy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
@ -1185,8 +1209,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + "-" + partYear;
|
combinedExpiryFillValue = fullMonth + "-" + partYear;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yyyy-mm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||||
@ -1194,8 +1219,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullYear + "-" + fullMonth;
|
combinedExpiryFillValue = fullYear + "-" + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yy-mm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||||
@ -1204,49 +1230,64 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = partYear + "-" + fullMonth;
|
combinedExpiryFillValue = partYear + "-" + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yyyymm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrLong[i] + CreditCardAutoFillConstants.MonthAbbr[i],
|
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||||
|
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullYear + fullMonth;
|
combinedExpiryFillValue = fullYear + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// yymm
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.YearAbbrShort[i] + CreditCardAutoFillConstants.MonthAbbr[i],
|
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||||
|
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = partYear + fullMonth;
|
combinedExpiryFillValue = partYear + fullMonth;
|
||||||
} else if (
|
} else if (
|
||||||
|
// mmyyyy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrLong[i],
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
|
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + fullYear;
|
combinedExpiryFillValue = fullMonth + fullYear;
|
||||||
} else if (
|
} else if (
|
||||||
|
// mmyy
|
||||||
this.fieldAttrsContain(
|
this.fieldAttrsContain(
|
||||||
fillFields.exp,
|
fillFields.exp,
|
||||||
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrShort[i],
|
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||||
|
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||||
) &&
|
) &&
|
||||||
partYear != null
|
partYear != null
|
||||||
) {
|
) {
|
||||||
exp = fullMonth + partYear;
|
combinedExpiryFillValue = fullMonth + partYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exp != null) {
|
if (combinedExpiryFillValue != null) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exp == null) {
|
// If none of the previous cases applied, set as default
|
||||||
exp = fullYear + "-" + fullMonth;
|
if (combinedExpiryFillValue == null) {
|
||||||
|
combinedExpiryFillValue = fullYear + "-" + fullMonth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields);
|
this.makeScriptActionWithValue(
|
||||||
|
fillScript,
|
||||||
|
combinedExpiryFillValue,
|
||||||
|
fillFields.exp,
|
||||||
|
filledFields,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fillScript;
|
return fillScript;
|
||||||
@ -1287,28 +1328,169 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
* Used when handling autofill on credit card fields. Determines whether
|
* Used when handling autofill on credit card fields. Determines whether
|
||||||
* the field has an attribute that matches the given value.
|
* the field has an attribute that matches the given value.
|
||||||
* @param {AutofillField} field
|
* @param {AutofillField} field
|
||||||
* @param {string} containsVal
|
* @param {string} containsValue
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private fieldAttrsContain(field: AutofillField, containsVal: string): boolean {
|
private fieldAttrsContain(field: AutofillField, containsValue: string): boolean {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let doesContain = false;
|
let doesContainValue = false;
|
||||||
CreditCardAutoFillConstants.CardAttributesExtended.forEach((attr) => {
|
CreditCardAutoFillConstants.CardAttributesExtended.forEach((attributeName) => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
if (doesContain || !field.hasOwnProperty(attr) || !field[attr]) {
|
if (doesContainValue || !field[attributeName]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let val = field[attr];
|
let fieldValue = field[attributeName];
|
||||||
val = val.replace(/ /g, "").toLowerCase();
|
fieldValue = fieldValue.replace(/ /g, "").toLowerCase();
|
||||||
doesContain = val.indexOf(containsVal) > -1;
|
doesContainValue = fieldValue.indexOf(containsValue) > -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return doesContain;
|
return doesContainValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string value representation of the combined card expiration month and year values
|
||||||
|
* in a format matching discovered guidance within the field attributes (typically provided for users).
|
||||||
|
*
|
||||||
|
* @param {CardView} cardCipher
|
||||||
|
* @param {AutofillField} field
|
||||||
|
*/
|
||||||
|
private generateCombinedExpiryValue(cardCipher: CardView, field: AutofillField): string {
|
||||||
|
/*
|
||||||
|
Some expectations of the passed stored card cipher view:
|
||||||
|
|
||||||
|
- At the time of writing, the stored card expiry year value (`expYear`)
|
||||||
|
can be any arbitrary string (no format validation). We may attempt some format
|
||||||
|
normalization here, but expect the user to have entered a string of integers
|
||||||
|
with a length of 2 or 4
|
||||||
|
|
||||||
|
- the `expiration` property cannot be used for autofill as it is an opinionated
|
||||||
|
format
|
||||||
|
|
||||||
|
- `expMonth` a stringified integer stored with no zero-padding and is not
|
||||||
|
zero-indexed (e.g. January is "1", not "01" or 0)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Expiry format options
|
||||||
|
let useMonthPadding = true;
|
||||||
|
let useYearFull = false;
|
||||||
|
let delimiter = "/";
|
||||||
|
let orderByYear = false;
|
||||||
|
|
||||||
|
// Because users are allowed to store truncated years, we need to make assumptions
|
||||||
|
// about the full year format when called for
|
||||||
|
const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
|
||||||
|
|
||||||
|
// Note, we construct the output rather than doing string replacement against the
|
||||||
|
// format guidance pattern to avoid edge cases that would output invalid values
|
||||||
|
const [
|
||||||
|
// The guidance parsed from the field properties regarding expiry format
|
||||||
|
expectedExpiryDateFormat,
|
||||||
|
// The (localized) date pattern set that was used to parse the expiry format guidance
|
||||||
|
expiryDateFormatPatterns,
|
||||||
|
] = this.getExpectedExpiryDateFormat(field);
|
||||||
|
|
||||||
|
if (expectedExpiryDateFormat) {
|
||||||
|
const { Month, MonthShort, Year } = expiryDateFormatPatterns;
|
||||||
|
|
||||||
|
const expiryDateDelimitersPattern =
|
||||||
|
"\\" + CreditCardAutoFillConstants.CardExpiryDateDelimiters.join("\\");
|
||||||
|
|
||||||
|
// assign the delimiter from the expected format string
|
||||||
|
delimiter =
|
||||||
|
expectedExpiryDateFormat.match(new RegExp(`[${expiryDateDelimitersPattern}]`, "g"))?.[0] ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
// check if the expected format starts with a month form
|
||||||
|
// order matters here; check long form first, since short form will match against long
|
||||||
|
if (expectedExpiryDateFormat.indexOf(Month + delimiter) === 0) {
|
||||||
|
useMonthPadding = true;
|
||||||
|
orderByYear = false;
|
||||||
|
} else if (expectedExpiryDateFormat.indexOf(MonthShort + delimiter) === 0) {
|
||||||
|
useMonthPadding = false;
|
||||||
|
orderByYear = false;
|
||||||
|
} else {
|
||||||
|
orderByYear = true;
|
||||||
|
|
||||||
|
// short form can match against long form, but long won't match against short
|
||||||
|
const containsLongMonthPattern = new RegExp(`${Month}`, "i");
|
||||||
|
useMonthPadding = containsLongMonthPattern.test(expectedExpiryDateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const containsLongYearPattern = new RegExp(`${Year}`, "i");
|
||||||
|
|
||||||
|
useYearFull = containsLongYearPattern.test(expectedExpiryDateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const month = useMonthPadding
|
||||||
|
? // Ensure zero-padding
|
||||||
|
("0" + cardCipher.expMonth).slice(-2)
|
||||||
|
: // Handle zero-padded stored month values, even though they are not _expected_ to be as such
|
||||||
|
cardCipher.expMonth.replaceAll("0", "");
|
||||||
|
// Note: assumes the user entered an `expYear` value with a length of either 2 or 4
|
||||||
|
const year = (currentCentury + cardCipher.expYear).slice(useYearFull ? -4 : -2);
|
||||||
|
|
||||||
|
const combinedExpiryFillValue = (orderByYear ? [year, month] : [month, year]).join(delimiter);
|
||||||
|
|
||||||
|
return combinedExpiryFillValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string value representation of discovered guidance for a combined month and year expiration value from the field attributes
|
||||||
|
*
|
||||||
|
* @param {AutofillField} field
|
||||||
|
*/
|
||||||
|
private getExpectedExpiryDateFormat(
|
||||||
|
field: AutofillField,
|
||||||
|
): [string | null, CardExpiryDateFormat | null] {
|
||||||
|
let expectedDateFormat = null;
|
||||||
|
let dateFormatPatterns = null;
|
||||||
|
|
||||||
|
const expiryDateDelimitersPattern =
|
||||||
|
"\\" + CreditCardAutoFillConstants.CardExpiryDateDelimiters.join("\\");
|
||||||
|
|
||||||
|
CreditCardAutoFillConstants.CardExpiryDateFormats.find((dateFormat) => {
|
||||||
|
dateFormatPatterns = dateFormat;
|
||||||
|
|
||||||
|
const { Month, MonthShort, YearShort, Year } = dateFormat;
|
||||||
|
|
||||||
|
// Non-exhaustive coverage of field guidances. Some uncovered edge cases: ". " delimiter, space-delimited delimiters ("mm / yyyy").
|
||||||
|
// We should consider if added whitespace is for improved readability of user-guidance or actually desired in the filled value.
|
||||||
|
// e.g. "/((mm|m)[\/\-\.\ ]{0,1}(yyyy|yy))|((yyyy|yy)[\/\-\.\ ]{0,1}(mm|m))/gi"
|
||||||
|
const dateFormatPattern = new RegExp(
|
||||||
|
`((${Month}|${MonthShort})[${expiryDateDelimitersPattern}]{0,1}(${Year}|${YearShort}))|((${Year}|${YearShort})[${expiryDateDelimitersPattern}]{0,1}(${Month}|${MonthShort}))`,
|
||||||
|
"gi",
|
||||||
|
);
|
||||||
|
|
||||||
|
return CreditCardAutoFillConstants.CardAttributesExtended.find((attributeName) => {
|
||||||
|
const fieldAttributeValue = field[attributeName];
|
||||||
|
|
||||||
|
const fieldAttributeMatch = fieldAttributeValue?.match(dateFormatPattern);
|
||||||
|
// break find as soon as a match is found
|
||||||
|
|
||||||
|
if (fieldAttributeMatch?.length) {
|
||||||
|
expectedDateFormat = fieldAttributeMatch[0];
|
||||||
|
|
||||||
|
// remove any irrelevant characters
|
||||||
|
const irrelevantExpiryCharactersPattern = new RegExp(
|
||||||
|
// "or digits" to ensure numbers are removed from guidance pattern, which aren't covered by ^\w
|
||||||
|
`[^\\w${expiryDateDelimitersPattern}]|[\\d]`,
|
||||||
|
"gi",
|
||||||
|
);
|
||||||
|
expectedDateFormat.replaceAll(irrelevantExpiryCharactersPattern, "");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [expectedDateFormat, dateFormatPatterns];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,6 +30,7 @@ export enum FeatureFlag {
|
|||||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||||
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
|
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
|
||||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||||
|
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||||
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
||||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||||
@ -76,6 +77,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||||
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
|
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
|
||||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||||
|
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||||
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
||||||
[FeatureFlag.StorageReseedRefactor]: FALSE,
|
[FeatureFlag.StorageReseedRefactor]: FALSE,
|
||||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||||
|
Loading…
Reference in New Issue
Block a user