[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",
|
||||
"htmlName",
|
||||
"htmlID",
|
||||
"title",
|
||||
"label-tag",
|
||||
"placeholder",
|
||||
"label-left",
|
||||
|
@ -299,6 +300,54 @@ export class CreditCardAutoFillConstants {
|
|||
"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.
|
||||
// 0: English, 1: Danish, 2: German/Dutch, 3: French/Spanish/Italian, 4: Russian, 5: Portuguese
|
||||
static readonly MonthAbbr = ["mm", "mm", "mm", "mm", "мм", "mm"];
|
||||
|
@ -306,6 +355,13 @@ export class CreditCardAutoFillConstants {
|
|||
static readonly YearAbbrLong = ["yyyy", "åååå", "jjjj", "aa", "гггг", "rrrr"];
|
||||
}
|
||||
|
||||
export type CardExpiryDateFormat = {
|
||||
Month: string;
|
||||
MonthShort: string;
|
||||
Year: string;
|
||||
YearShort: string;
|
||||
};
|
||||
|
||||
export class IdentityAutoFillConstants {
|
||||
static readonly IdentityAttributes: string[] = [
|
||||
"autoCompleteType",
|
||||
|
|
|
@ -2475,10 +2475,10 @@ describe("AutofillService", () => {
|
|||
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;
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2499,7 +2499,7 @@ describe("AutofillService", () => {
|
|||
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({
|
||||
opid: "span-field",
|
||||
form: "validFormId",
|
||||
|
@ -2510,7 +2510,7 @@ describe("AutofillService", () => {
|
|||
pageDetails.fields = [spanField];
|
||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2522,7 +2522,7 @@ describe("AutofillService", () => {
|
|||
});
|
||||
|
||||
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({
|
||||
opid: `${excludedType}-field`,
|
||||
form: "validFormId",
|
||||
|
@ -2533,7 +2533,7 @@ describe("AutofillService", () => {
|
|||
pageDetails.fields = [invalidField];
|
||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
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({
|
||||
opid: "invalid-field",
|
||||
form: "validFormId",
|
||||
|
@ -2561,7 +2561,7 @@ describe("AutofillService", () => {
|
|||
jest.spyOn(AutofillService, "forCustomFieldsOnly");
|
||||
jest.spyOn(AutofillService, "isExcludedFieldType");
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2663,8 +2663,8 @@ describe("AutofillService", () => {
|
|||
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
|
||||
});
|
||||
|
||||
it("returns a fill script containing all of the passed card fields", () => {
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
it("returns a fill script containing all of the passed card fields", async () => {
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2745,11 +2745,11 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2759,12 +2759,12 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||
expMonthField.selectInfo.options.push(["", ""]);
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2774,12 +2774,12 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expMonthField.selectInfo.options[4] = ["May", testValue];
|
||||
expMonthField.selectInfo.options.unshift(["", ""]);
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2789,13 +2789,13 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expMonthField.selectInfo = null;
|
||||
expMonthField.placeholder = "mm";
|
||||
expMonthField.maxLength = 2;
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2830,12 +2830,12 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expYearField.selectInfo.options[1] = ["2024", someTestValue];
|
||||
options.cipher.card.expYear = someTestValue;
|
||||
|
||||
let value = autofillService["generateCardFillScript"](
|
||||
let value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2846,7 +2846,7 @@ describe("AutofillService", () => {
|
|||
|
||||
expYearField.selectInfo.options[1] = [someTestValue, "2024"];
|
||||
|
||||
value = autofillService["generateCardFillScript"](
|
||||
value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2856,12 +2856,12 @@ describe("AutofillService", () => {
|
|||
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";
|
||||
expYearField.selectInfo.options.push(["The year 2026", yearValue]);
|
||||
options.cipher.card.expYear = "2026";
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2871,13 +2871,13 @@ describe("AutofillService", () => {
|
|||
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 colonSeparatedYearValue = `2:0${yearValue}`;
|
||||
expYearField.selectInfo.options.push(["The year 2026", colonSeparatedYearValue]);
|
||||
options.cipher.card.expYear = yearValue;
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
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";
|
||||
expYearField.selectInfo = null;
|
||||
expYearField.placeholder = "yyyy";
|
||||
expYearField.maxLength = 4;
|
||||
options.cipher.card.expYear = yearValue;
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
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";
|
||||
expYearField.selectInfo = null;
|
||||
expYearField.placeholder = "yy";
|
||||
expYearField.maxLength = 2;
|
||||
options.cipher.card.expYear = `20${yearValue}`;
|
||||
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
const value = await autofillService["generateCardFillScript"](
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2930,11 +2930,26 @@ describe("AutofillService", () => {
|
|||
});
|
||||
});
|
||||
|
||||
const expectedDateFormats = [
|
||||
["mm/yyyy", "05/2024"],
|
||||
["mm/yy", "05/24"],
|
||||
["yyyy/mm", "2024/05"],
|
||||
["yy/mm", "24/05"],
|
||||
["mm-yyyy", "05-2024"],
|
||||
["mm-yy", "05-24"],
|
||||
["yyyy-mm", "2024-05"],
|
||||
["yy-mm", "24-05"],
|
||||
["yyyymm", "202405"],
|
||||
["yymm", "2405"],
|
||||
["mmyyyy", "052024"],
|
||||
["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",
|
||||
|
@ -2949,23 +2964,11 @@ describe("AutofillService", () => {
|
|||
options.cipher.card.expYear = "2024";
|
||||
});
|
||||
|
||||
const expectedDateFormats = [
|
||||
["mm/yyyy", "05/2024"],
|
||||
["mm/yy", "05/24"],
|
||||
["yyyy/mm", "2024/05"],
|
||||
["yy/mm", "24/05"],
|
||||
["mm-yyyy", "05-2024"],
|
||||
["mm-yy", "05-24"],
|
||||
["yyyy-mm", "2024-05"],
|
||||
["yy-mm", "24-05"],
|
||||
["yyyymm", "202405"],
|
||||
["yymm", "2405"],
|
||||
["mmyyyy", "052024"],
|
||||
["mmyy", "0524"],
|
||||
];
|
||||
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];
|
||||
|
||||
// test alternate stored cipher value formats
|
||||
if (index === 0) {
|
||||
options.cipher.card.expYear = "24";
|
||||
}
|
||||
|
@ -2973,7 +2976,13 @@ describe("AutofillService", () => {
|
|||
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,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
|
@ -2984,17 +2993,128 @@ describe("AutofillService", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", () => {
|
||||
const value = autofillService["generateCardFillScript"](
|
||||
it("returns an expiration date format matching `yyyy-mm` 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(false);
|
||||
|
||||
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", () => {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
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 { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
|
@ -50,6 +51,7 @@ import {
|
|||
} from "./abstractions/autofill.service";
|
||||
import {
|
||||
AutoFillConstants,
|
||||
CardExpiryDateFormat,
|
||||
CreditCardAutoFillConstants,
|
||||
IdentityAutoFillConstants,
|
||||
} from "./autofill-constants";
|
||||
|
@ -721,7 +723,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options);
|
||||
fillScript = await this.generateCardFillScript(
|
||||
fillScript,
|
||||
pageDetails,
|
||||
filledFields,
|
||||
options,
|
||||
);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
fillScript = await this.generateIdentityFillScript(
|
||||
|
@ -937,12 +944,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
* @returns {AutofillScript|null}
|
||||
* @private
|
||||
*/
|
||||
private generateCardFillScript(
|
||||
private async generateCardFillScript(
|
||||
fillScript: AutofillScript,
|
||||
pageDetails: AutofillPageDetails,
|
||||
filledFields: { [id: string]: AutofillField },
|
||||
options: GenerateFillScriptOptions,
|
||||
): AutofillScript | null {
|
||||
): Promise<AutofillScript | null> {
|
||||
if (!options.cipher.card) {
|
||||
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, "brand");
|
||||
|
||||
// There is an expiration month field and the cipher has an expiration month value
|
||||
if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) {
|
||||
let expMonth: string = card.expMonth;
|
||||
|
||||
|
@ -1065,6 +1073,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
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)) {
|
||||
let expYear: string = card.expYear;
|
||||
if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) {
|
||||
|
@ -1111,142 +1120,174 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
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 (
|
||||
fillFields.exp &&
|
||||
AutofillService.hasValue(card.expMonth) &&
|
||||
AutofillService.hasValue(card.expYear)
|
||||
) {
|
||||
const fullMonth = ("0" + card.expMonth).slice(-2);
|
||||
let combinedExpiryFillValue = null;
|
||||
|
||||
let fullYear: string = card.expYear;
|
||||
let partYear: string = null;
|
||||
if (fullYear.length === 2) {
|
||||
partYear = fullYear;
|
||||
fullYear = normalizeExpiryYearFormat(fullYear);
|
||||
} else if (fullYear.length === 4) {
|
||||
partYear = fullYear.substr(2, 2);
|
||||
}
|
||||
const enableNewCardCombinedExpiryAutofill = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.EnableNewCardCombinedExpiryAutofill,
|
||||
);
|
||||
|
||||
let exp: string = null;
|
||||
for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) {
|
||||
if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
exp = fullMonth + "/" + fullYear;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = fullMonth + "/" + partYear;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
exp = fullYear + "/" + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = partYear + "/" + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
exp = fullMonth + "-" + fullYear;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = fullMonth + "-" + partYear;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
exp = fullYear + "-" + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = partYear + "-" + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] + CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
exp = fullYear + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] + CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = partYear + fullMonth;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
exp = fullMonth + fullYear;
|
||||
} else if (
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
exp = fullMonth + partYear;
|
||||
if (enableNewCardCombinedExpiryAutofill) {
|
||||
combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp);
|
||||
} else {
|
||||
const fullMonth = ("0" + card.expMonth).slice(-2);
|
||||
|
||||
let fullYear: string = card.expYear;
|
||||
let partYear: string = null;
|
||||
if (fullYear.length === 2) {
|
||||
partYear = fullYear;
|
||||
fullYear = normalizeExpiryYearFormat(fullYear);
|
||||
} else if (fullYear.length === 4) {
|
||||
partYear = fullYear.substr(2, 2);
|
||||
}
|
||||
|
||||
if (exp != null) {
|
||||
break;
|
||||
for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) {
|
||||
if (
|
||||
// mm/yyyy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + "/" + fullYear;
|
||||
} else if (
|
||||
// mm/yy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + "/" + partYear;
|
||||
} else if (
|
||||
// yyyy/mm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullYear + "/" + fullMonth;
|
||||
} else if (
|
||||
// yy/mm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||
"/" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = partYear + "/" + fullMonth;
|
||||
} else if (
|
||||
// mm-yyyy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + "-" + fullYear;
|
||||
} else if (
|
||||
// mm-yy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + "-" + partYear;
|
||||
} else if (
|
||||
// yyyy-mm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullYear + "-" + fullMonth;
|
||||
} else if (
|
||||
// yy-mm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||
"-" +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = partYear + "-" + fullMonth;
|
||||
} else if (
|
||||
// yyyymm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i] +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullYear + fullMonth;
|
||||
} else if (
|
||||
// yymm
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i] +
|
||||
CreditCardAutoFillConstants.MonthAbbr[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = partYear + fullMonth;
|
||||
} else if (
|
||||
// mmyyyy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
CreditCardAutoFillConstants.YearAbbrLong[i],
|
||||
)
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + fullYear;
|
||||
} else if (
|
||||
// mmyy
|
||||
this.fieldAttrsContain(
|
||||
fillFields.exp,
|
||||
CreditCardAutoFillConstants.MonthAbbr[i] +
|
||||
CreditCardAutoFillConstants.YearAbbrShort[i],
|
||||
) &&
|
||||
partYear != null
|
||||
) {
|
||||
combinedExpiryFillValue = fullMonth + partYear;
|
||||
}
|
||||
|
||||
if (combinedExpiryFillValue != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the previous cases applied, set as default
|
||||
if (combinedExpiryFillValue == null) {
|
||||
combinedExpiryFillValue = fullYear + "-" + fullMonth;
|
||||
}
|
||||
}
|
||||
|
||||
if (exp == null) {
|
||||
exp = fullYear + "-" + fullMonth;
|
||||
}
|
||||
|
||||
this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields);
|
||||
this.makeScriptActionWithValue(
|
||||
fillScript,
|
||||
combinedExpiryFillValue,
|
||||
fillFields.exp,
|
||||
filledFields,
|
||||
);
|
||||
}
|
||||
|
||||
return fillScript;
|
||||
|
@ -1287,28 +1328,169 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
* Used when handling autofill on credit card fields. Determines whether
|
||||
* the field has an attribute that matches the given value.
|
||||
* @param {AutofillField} field
|
||||
* @param {string} containsVal
|
||||
* @param {string} containsValue
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
private fieldAttrsContain(field: AutofillField, containsVal: string): boolean {
|
||||
private fieldAttrsContain(field: AutofillField, containsValue: string): boolean {
|
||||
if (!field) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let doesContain = false;
|
||||
CreditCardAutoFillConstants.CardAttributesExtended.forEach((attr) => {
|
||||
// eslint-disable-next-line
|
||||
if (doesContain || !field.hasOwnProperty(attr) || !field[attr]) {
|
||||
let doesContainValue = false;
|
||||
CreditCardAutoFillConstants.CardAttributesExtended.forEach((attributeName) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (doesContainValue || !field[attributeName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let val = field[attr];
|
||||
val = val.replace(/ /g, "").toLowerCase();
|
||||
doesContain = val.indexOf(containsVal) > -1;
|
||||
let fieldValue = field[attributeName];
|
||||
fieldValue = fieldValue.replace(/ /g, "").toLowerCase();
|
||||
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",
|
||||
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
|
||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
|
@ -76,6 +77,7 @@ export const DefaultFeatureFlagValue = {
|
|||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
|
||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
||||
[FeatureFlag.StorageReseedRefactor]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||
|
|
Loading…
Reference in New Issue