From 28dc0870952aae38b5757eb10d9acabb6545903f Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Tue, 12 Nov 2024 16:16:24 +0100 Subject: [PATCH 01/12] feat: add API call --- dist/autofill-debug.js | 12 +- dist/autofill.js | 9 +- src/device-interface.d.ts | 2 +- .../__generated__/deviceApiCalls.js | 6 + .../__generated__/validators-ts.ts | 6 + .../__generated__/validators.zod.js | 3 +- src/deviceApiCalls/api.json | 183 ++++++++++++++---- .../storeTemporaryPartialFormData.params.json | 19 ++ .../Resources/assets/autofill-debug.js | 12 +- swift-package/Resources/assets/autofill.js | 9 +- 10 files changed, 212 insertions(+), 49 deletions(-) create mode 100644 src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 30b05078f..d16d2f277 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -17716,7 +17716,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -17968,7 +17968,14 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } +/** + * @extends {DeviceApiCall} + */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; +class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { + method = "storeTemporaryPartialFormData"; +} +exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[function(require,module,exports){ "use strict"; @@ -18376,7 +18383,8 @@ const apiSchema = exports.apiSchema = _zod.z.object({ ShowInContextEmailProtectionSignupPrompt: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional() + })).optional(), + storeTemporaryPartialFormData: _zod.z.record(_zod.z.unknown()).optional() }); },{"zod":9}],70:[function(require,module,exports){ diff --git a/dist/autofill.js b/dist/autofill.js index 715d75b26..c7063a2ef 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -13550,7 +13550,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -13802,7 +13802,14 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } +/** + * @extends {DeviceApiCall} + */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; +class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { + method = "storeTemporaryPartialFormData"; +} +exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[function(require,module,exports){ "use strict"; diff --git a/src/device-interface.d.ts b/src/device-interface.d.ts index 6d9f5d7d7..d16fd88a1 100644 --- a/src/device-interface.d.ts +++ b/src/device-interface.d.ts @@ -71,7 +71,7 @@ interface DataStorageObject { credentials?: CredentialsObject; creditCards?: CreditCardObject; identities?: IdentityObject; - trigger?: 'formSubmission' | 'passwordGeneration' | 'emailProtection'; + trigger?: 'partialFormSave' | 'formSubmission' | 'passwordGeneration' | 'emailProtection'; } interface InternalDataStorageObject { diff --git a/src/deviceApiCalls/__generated__/deviceApiCalls.js b/src/deviceApiCalls/__generated__/deviceApiCalls.js index e0a61b739..3411a3d51 100644 --- a/src/deviceApiCalls/__generated__/deviceApiCalls.js +++ b/src/deviceApiCalls/__generated__/deviceApiCalls.js @@ -243,4 +243,10 @@ export class ShowInContextEmailProtectionSignupPromptCall extends DeviceApiCall method = "ShowInContextEmailProtectionSignupPrompt" id = "ShowInContextEmailProtectionSignupPromptResponse" resultValidator = showInContextEmailProtectionSignupPromptSchema +} +/** + * @extends {DeviceApiCall} + */ +export class StoreTemporaryPartialFormDataCall extends DeviceApiCall { + method = "storeTemporaryPartialFormData" } \ No newline at end of file diff --git a/src/deviceApiCalls/__generated__/validators-ts.ts b/src/deviceApiCalls/__generated__/validators-ts.ts index a7e1d8c4d..178d7b2bf 100644 --- a/src/deviceApiCalls/__generated__/validators-ts.ts +++ b/src/deviceApiCalls/__generated__/validators-ts.ts @@ -248,6 +248,12 @@ export interface API { resultValidator?: ShowInContextEmailProtectionSignupPrompt; [k: string]: unknown; }; + /** + * Used to store temporary partial form data (username or email) to supplement the signup completion at a later time + */ + storeTemporaryPartialFormData?: { + [k: string]: unknown; + }; } /** * Parameters for the addDebugFlag method diff --git a/src/deviceApiCalls/__generated__/validators.zod.js b/src/deviceApiCalls/__generated__/validators.zod.js index 6724b04fc..a2ffc7de2 100644 --- a/src/deviceApiCalls/__generated__/validators.zod.js +++ b/src/deviceApiCalls/__generated__/validators.zod.js @@ -435,5 +435,6 @@ export const apiSchema = z.object({ ShowInContextEmailProtectionSignupPrompt: z.record(z.unknown()).and(z.object({ id: z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional() + })).optional(), + storeTemporaryPartialFormData: z.record(z.unknown()).optional() }); diff --git a/src/deviceApiCalls/api.json b/src/deviceApiCalls/api.json index e6151e868..2137bbaa4 100644 --- a/src/deviceApiCalls/api.json +++ b/src/deviceApiCalls/api.json @@ -9,7 +9,9 @@ "type": "object", "description": "Register a new debug flag that will be included in breakage reports", "properties": { - "paramsValidator": { "$ref": "./schemas/addDebugFlag.params.json" } + "paramsValidator": { + "$ref": "./schemas/addDebugFlag.params.json" + } } }, "getAutofillData": { @@ -19,59 +21,91 @@ "type": "string", "const": "getAutofillDataResponse" }, - "paramsValidator": { "$ref": "./schemas/getAutofillData.params.json" }, - "resultValidator": { "$ref": "./schemas/getAutofillData.result.json" } + "paramsValidator": { + "$ref": "./schemas/getAutofillData.params.json" + }, + "resultValidator": { + "$ref": "./schemas/getAutofillData.result.json" + } } }, "getRuntimeConfiguration": { "type": "object", "properties": { - "id": { "type": "string", "const": "getRuntimeConfigurationResponse"}, - "resultValidator": { "$ref": "./schemas/getRuntimeConfiguration.result.json" } + "id": { + "type": "string", + "const": "getRuntimeConfigurationResponse" + }, + "resultValidator": { + "$ref": "./schemas/getRuntimeConfiguration.result.json" + } } }, "storeFormData": { "type": "object", "properties": { - "paramsValidator": { "$ref": "./schemas/storeFormData.params.json" } + "paramsValidator": { + "$ref": "./schemas/storeFormData.params.json" + } } }, "getAvailableInputTypes": { "type": "object", "properties": { - "id": { "type": "string", "const": "getAvailableInputTypesResponse"}, - "resultValidator": { "$ref": "./schemas/getAvailableInputTypes.result.json" } + "id": { + "type": "string", + "const": "getAvailableInputTypesResponse" + }, + "resultValidator": { + "$ref": "./schemas/getAvailableInputTypes.result.json" + } } }, "getAutofillInitData": { "type": "object", "description": "This is called inside an overlay (eg: on Windows or soon also on macOS) to retrieve available data", "properties": { - "id": { "type": "string", "const": "getAutofillInitDataResponse"}, - "resultValidator": { "$ref": "./schemas/getAutofillInitData.result.json" } + "id": { + "type": "string", + "const": "getAutofillInitDataResponse" + }, + "resultValidator": { + "$ref": "./schemas/getAutofillInitData.result.json" + } } }, "getAutofillCredentials": { "type": "object", "description": "Used to retrieve a specific set of credentials", "properties": { - "id": { "type": "string", "const": "getAutofillCredentialsResponse"}, - "paramsValidator": { "$ref": "./schemas/getAutofillCredentials.params.json" }, - "resultValidator": { "$ref": "./schemas/getAutofillCredentials.result.json" } + "id": { + "type": "string", + "const": "getAutofillCredentialsResponse" + }, + "paramsValidator": { + "$ref": "./schemas/getAutofillCredentials.params.json" + }, + "resultValidator": { + "$ref": "./schemas/getAutofillCredentials.result.json" + } } }, "setSize": { "type": "object", "description": "Used by Windows to communicate the desired size of the overlay to the native side", "properties": { - "paramsValidator": { "$ref": "./schemas/setSize.params.json" } + "paramsValidator": { + "$ref": "./schemas/setSize.params.json" + } } }, "selectedDetail": { "type": "object", "description": "Used by Windows to communicate a selected autofill item to the native side", "properties": { - "paramsValidator": { "$ref": "./schemas/selectedDetail.params.json" } + "paramsValidator": { + "$ref": "./schemas/selectedDetail.params.json" + } } }, "closeAutofillParent": { @@ -81,34 +115,53 @@ "askToUnlockProvider": { "type": "object", "properties": { - "id": { "type": "string", "const": "askToUnlockProviderResponse"}, - "resultValidator": { "$ref": "./schemas/askToUnlockProvider.result.json" } + "id": { + "type": "string", + "const": "askToUnlockProviderResponse" + }, + "resultValidator": { + "$ref": "./schemas/askToUnlockProvider.result.json" + } } }, "checkCredentialsProviderStatus": { "type": "object", "properties": { - "id": { "type": "string", "const": "checkCredentialsProviderStatusResponse"}, - "resultValidator": { "$ref": "./schemas/checkCredentialsProviderStatus.result.json" } + "id": { + "type": "string", + "const": "checkCredentialsProviderStatusResponse" + }, + "resultValidator": { + "$ref": "./schemas/checkCredentialsProviderStatus.result.json" + } } }, "sendJSPixel": { "type": "object", "properties": { - "paramsValidator": { "$ref": "./schemas/sendJSPixel.params.json" } + "paramsValidator": { + "$ref": "./schemas/sendJSPixel.params.json" + } } }, "setIncontextSignupPermanentlyDismissedAt": { "type": "object", "properties": { - "paramsValidator": { "$ref": "./schemas/setIncontextSignupPermanentlyDismissedAt.params.json" } + "paramsValidator": { + "$ref": "./schemas/setIncontextSignupPermanentlyDismissedAt.params.json" + } } }, "getIncontextSignupDismissedAt": { "type": "object", "properties": { - "id": { "type": "string", "const": "getIncontextSignupDismissedAt"}, - "resultValidator": { "$ref": "./schemas/getIncontextSignupDismissedAt.result.json" } + "id": { + "type": "string", + "const": "getIncontextSignupDismissedAt" + }, + "resultValidator": { + "$ref": "./schemas/getIncontextSignupDismissedAt.result.json" + } } }, "autofillSettings": { @@ -118,7 +171,9 @@ "type": "boolean", "const": true }, - "resultValidator": { "$ref": "./schemas/autofill-settings.json" } + "resultValidator": { + "$ref": "./schemas/autofill-settings.json" + } } }, "getAlias": { @@ -128,8 +183,12 @@ "type": "boolean", "const": true }, - "paramValidator": { "$ref": "./schemas/getAlias.params.json" }, - "resultValidator": { "$ref": "./schemas/getAlias.result.json" } + "paramValidator": { + "$ref": "./schemas/getAlias.params.json" + }, + "resultValidator": { + "$ref": "./schemas/getAlias.result.json" + } } }, "openManagePasswords": { @@ -156,8 +215,13 @@ "type": "object", "description": "Used to store Email Protection auth credentials (logging in)", "properties": { - "id": { "type": "string", "const": "emailProtectionStoreUserDataResponse"}, - "paramsValidator": { "$ref": "./schemas/emailProtectionStoreUserData.params.json" } + "id": { + "type": "string", + "const": "emailProtectionStoreUserDataResponse" + }, + "paramsValidator": { + "$ref": "./schemas/emailProtectionStoreUserData.params.json" + } } }, "emailProtectionRemoveUserData": { @@ -168,40 +232,65 @@ "type": "object", "description": "Used to get check if a user is logged in to Email Protection", "properties": { - "id": { "type": "string", "const": "emailProtectionGetIsLoggedInResponse"}, - "resultValidator": { "$ref": "./schemas/emailProtectionGetIsLoggedIn.result.json" } + "id": { + "type": "string", + "const": "emailProtectionGetIsLoggedInResponse" + }, + "resultValidator": { + "$ref": "./schemas/emailProtectionGetIsLoggedIn.result.json" + } } }, "emailProtectionGetUserData": { "type": "object", "description": "Used to get Email Protection auth credentials", "properties": { - "id": { "type": "string", "const": "emailProtectionGetUserDataResponse"}, - "resultValidator": { "$ref": "./schemas/emailProtectionGetUserData.result.json" } + "id": { + "type": "string", + "const": "emailProtectionGetUserDataResponse" + }, + "resultValidator": { + "$ref": "./schemas/emailProtectionGetUserData.result.json" + } } }, "emailProtectionGetCapabilities": { "type": "object", "description": "Used by the Email Protection web app to determine which API functionality is available", "properties": { - "id": { "type": "string", "const": "emailProtectionGetCapabilitiesResponse"}, - "resultValidator": { "$ref": "./schemas/emailProtectionGetCapabilities.result.json" } + "id": { + "type": "string", + "const": "emailProtectionGetCapabilitiesResponse" + }, + "resultValidator": { + "$ref": "./schemas/emailProtectionGetCapabilities.result.json" + } } }, "emailProtectionGetAddresses": { "type": "object", "description": "Used to get both Email Protection addresses (personal and private)", "properties": { - "id": { "type": "string", "const": "emailProtectionGetAddressesResponse"}, - "resultValidator": { "$ref": "./schemas/emailProtectionGetAddresses.result.json" } + "id": { + "type": "string", + "const": "emailProtectionGetAddressesResponse" + }, + "resultValidator": { + "$ref": "./schemas/emailProtectionGetAddresses.result.json" + } } }, "emailProtectionRefreshPrivateAddress": { "type": "object", "description": "Used to refresh Email Protection private address and get both Email Protection addresses (personal and private)", "properties": { - "id": { "type": "string", "const": "emailProtectionRefreshPrivateAddressResponse"}, - "resultValidator": { "$ref": "./schemas/emailProtectionRefreshPrivateAddress.result.json" } + "id": { + "type": "string", + "const": "emailProtectionRefreshPrivateAddressResponse" + }, + "resultValidator": { + "$ref": "./schemas/emailProtectionRefreshPrivateAddress.result.json" + } } }, "startEmailProtectionSignup": { @@ -216,9 +305,21 @@ "type": "object", "description": "Used by Android to open the in-context signup prompt and report back when completed", "properties": { - "id": { "type": "string", "const": "ShowInContextEmailProtectionSignupPromptResponse"}, - "resultValidator": { "$ref": "./schemas/showInContextEmailProtectionSignupPrompt.result.json" } + "id": { + "type": "string", + "const": "ShowInContextEmailProtectionSignupPromptResponse" + }, + "resultValidator": { + "$ref": "./schemas/showInContextEmailProtectionSignupPrompt.result.json" + } + } + }, + "storeTemporaryPartialFormData": { + "type": "object", + "description": "Used to store temporary partial form data (username or email) to supplement the signup completion at a later time", + "paramsValidator": { + "$ref": "./schemas/storeTemporaryPartialFormData.params.json" } } } -} +} \ No newline at end of file diff --git a/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json b/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json new file mode 100644 index 000000000..7d129165e --- /dev/null +++ b/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StoreTemporaryPartialFormData", + "type": "object", + "description": "Autofill could send this data at any point. \n\nIt will **not** listen for a response, it's expected that the native side will handle", + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "additionalProperties": false + }, + "trigger": { + "type": "string", + "enum": [ + "partialFormSave" + ] + } + } +} \ No newline at end of file diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 30b05078f..d16d2f277 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -17716,7 +17716,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -17968,7 +17968,14 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } +/** + * @extends {DeviceApiCall} + */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; +class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { + method = "storeTemporaryPartialFormData"; +} +exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[function(require,module,exports){ "use strict"; @@ -18376,7 +18383,8 @@ const apiSchema = exports.apiSchema = _zod.z.object({ ShowInContextEmailProtectionSignupPrompt: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional() + })).optional(), + storeTemporaryPartialFormData: _zod.z.record(_zod.z.unknown()).optional() }); },{"zod":9}],70:[function(require,module,exports){ diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 715d75b26..c7063a2ef 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -13550,7 +13550,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -13802,7 +13802,14 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } +/** + * @extends {DeviceApiCall} + */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; +class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { + method = "storeTemporaryPartialFormData"; +} +exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[function(require,module,exports){ "use strict"; From dbd1b6668636b10ac803403f65023bd7a3f029e9 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Wed, 13 Nov 2024 13:06:19 +0100 Subject: [PATCH 02/12] feat: allow partial save in form submission --- dist/autofill-debug.js | 12 ++++++++---- dist/autofill.js | 10 +++++++--- src/DeviceInterface/InterfacePrototype.js | 5 ++++- src/Form/formatters.js | 2 +- src/autofill-utils.js | 3 ++- src/device-interface.d.ts | 2 +- src/deviceApiCalls/__generated__/validators-ts.ts | 2 +- src/deviceApiCalls/__generated__/validators.zod.js | 2 +- src/deviceApiCalls/schemas/storeFormData.params.json | 3 ++- swift-package/Resources/assets/autofill-debug.js | 12 ++++++++---- swift-package/Resources/assets/autofill.js | 10 +++++++--- 11 files changed, 42 insertions(+), 21 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index d16d2f277..6d11d7f40 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9282,7 +9282,10 @@ class InterfacePrototype { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -11890,7 +11893,7 @@ const shouldStoreCredentials = _ref3 => { let { credentials } = _ref3; - return Boolean(credentials.password); + return Boolean(credentials.password) || Boolean(credentials.username); }; /** @@ -17413,7 +17416,8 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return readDebugSetting('ddg-autofill-debug'); + return true; + // return readDebugSetting('ddg-autofill-debug'); } /** @@ -18196,7 +18200,7 @@ const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _z }); const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ credentials: outgoingCredentialsSchema.optional(), - trigger: _zod.z.union([_zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() + trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() }); const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ type: _zod.z.literal("getAvailableInputTypesResponse").optional(), diff --git a/dist/autofill.js b/dist/autofill.js index c7063a2ef..ecc9b8b76 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5116,7 +5116,10 @@ class InterfacePrototype { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -7724,7 +7727,7 @@ const shouldStoreCredentials = _ref3 => { let { credentials } = _ref3; - return Boolean(credentials.password); + return Boolean(credentials.password) || Boolean(credentials.username); }; /** @@ -13247,7 +13250,8 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return readDebugSetting('ddg-autofill-debug'); + return true; + // return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index afeb0531d..a37abb3ec 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -806,7 +806,10 @@ class InterfacePrototype { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated, }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } diff --git a/src/Form/formatters.js b/src/Form/formatters.js index a9ad6eb02..bf4563ec8 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -163,7 +163,7 @@ const getMMAndYYYYFromString = (expiration) => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCredentials = ({ credentials }) => Boolean(credentials.password); +const shouldStoreCredentials = ({ credentials }) => Boolean(credentials.password) || Boolean(credentials.username); /** * @param {InternalDataStorageObject} credentials diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 5a4984052..09255eb0a 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -426,7 +426,8 @@ const wasAutofilledByChrome = (input) => { * @returns {boolean} */ function shouldLog() { - return readDebugSetting('ddg-autofill-debug'); + return true; + // return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/src/device-interface.d.ts b/src/device-interface.d.ts index d16fd88a1..1d03ef557 100644 --- a/src/device-interface.d.ts +++ b/src/device-interface.d.ts @@ -71,7 +71,7 @@ interface DataStorageObject { credentials?: CredentialsObject; creditCards?: CreditCardObject; identities?: IdentityObject; - trigger?: 'partialFormSave' | 'formSubmission' | 'passwordGeneration' | 'emailProtection'; + trigger?: 'partialSave' | 'formSubmission' | 'passwordGeneration' | 'emailProtection'; } interface InternalDataStorageObject { diff --git a/src/deviceApiCalls/__generated__/validators-ts.ts b/src/deviceApiCalls/__generated__/validators-ts.ts index 178d7b2bf..c759e2481 100644 --- a/src/deviceApiCalls/__generated__/validators-ts.ts +++ b/src/deviceApiCalls/__generated__/validators-ts.ts @@ -395,7 +395,7 @@ export interface UserPreferences { */ export interface StoreFormData { credentials?: OutgoingCredentials; - trigger?: "formSubmission" | "passwordGeneration" | "emailProtection"; + trigger?: "partialSave" | "formSubmission" | "passwordGeneration" | "emailProtection"; } export interface OutgoingCredentials { /** diff --git a/src/deviceApiCalls/__generated__/validators.zod.js b/src/deviceApiCalls/__generated__/validators.zod.js index a2ffc7de2..0ca4538d5 100644 --- a/src/deviceApiCalls/__generated__/validators.zod.js +++ b/src/deviceApiCalls/__generated__/validators.zod.js @@ -233,7 +233,7 @@ export const getAutofillDataResponseSchema = z.object({ export const storeFormDataSchema = z.object({ credentials: outgoingCredentialsSchema.optional(), - trigger: z.union([z.literal("formSubmission"), z.literal("passwordGeneration"), z.literal("emailProtection")]).optional() + trigger: z.union([z.literal("partialSave"), z.literal("formSubmission"), z.literal("passwordGeneration"), z.literal("emailProtection")]).optional() }); export const getAvailableInputTypesResultSchema = z.object({ diff --git a/src/deviceApiCalls/schemas/storeFormData.params.json b/src/deviceApiCalls/schemas/storeFormData.params.json index 1c68e9ce1..8b86c5573 100644 --- a/src/deviceApiCalls/schemas/storeFormData.params.json +++ b/src/deviceApiCalls/schemas/storeFormData.params.json @@ -23,10 +23,11 @@ "trigger": { "type": "string", "enum": [ + "partialSave", "formSubmission", "passwordGeneration", "emailProtection" ] } } -} +} \ No newline at end of file diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index d16d2f277..6d11d7f40 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9282,7 +9282,10 @@ class InterfacePrototype { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -11890,7 +11893,7 @@ const shouldStoreCredentials = _ref3 => { let { credentials } = _ref3; - return Boolean(credentials.password); + return Boolean(credentials.password) || Boolean(credentials.username); }; /** @@ -17413,7 +17416,8 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return readDebugSetting('ddg-autofill-debug'); + return true; + // return readDebugSetting('ddg-autofill-debug'); } /** @@ -18196,7 +18200,7 @@ const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _z }); const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ credentials: outgoingCredentialsSchema.optional(), - trigger: _zod.z.union([_zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() + trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() }); const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ type: _zod.z.literal("getAvailableInputTypesResponse").optional(), diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index c7063a2ef..ecc9b8b76 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5116,7 +5116,10 @@ class InterfacePrototype { password: this.passwordGenerator.password, username: this.emailProtection.lastGenerated }); - this.storeFormData(formData, 'formSubmission'); + + // If credentials has only username field, and no password field, then trigger is a partialSave + const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + this.storeFormData(formData, trigger); } } @@ -7724,7 +7727,7 @@ const shouldStoreCredentials = _ref3 => { let { credentials } = _ref3; - return Boolean(credentials.password); + return Boolean(credentials.password) || Boolean(credentials.username); }; /** @@ -13247,7 +13250,8 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return readDebugSetting('ddg-autofill-debug'); + return true; + // return readDebugSetting('ddg-autofill-debug'); } /** From 65bead140aba1ae802670bd9aac1f7e509ef8250 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Wed, 13 Nov 2024 14:24:56 +0100 Subject: [PATCH 03/12] fix: don't need additional API --- dist/autofill-debug.js | 12 ++---------- dist/autofill.js | 9 +-------- .../__generated__/deviceApiCalls.js | 6 ------ .../__generated__/validators-ts.ts | 6 ------ .../__generated__/validators.zod.js | 3 +-- src/deviceApiCalls/api.json | 7 ------- .../storeTemporaryPartialFormData.params.json | 19 ------------------- .../Resources/assets/autofill-debug.js | 12 ++---------- swift-package/Resources/assets/autofill.js | 9 +-------- 9 files changed, 7 insertions(+), 76 deletions(-) delete mode 100644 src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 6d11d7f40..ad2a3e1f5 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -17720,7 +17720,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -17972,14 +17972,7 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } -/** - * @extends {DeviceApiCall} - */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { - method = "storeTemporaryPartialFormData"; -} -exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[function(require,module,exports){ "use strict"; @@ -18387,8 +18380,7 @@ const apiSchema = exports.apiSchema = _zod.z.object({ ShowInContextEmailProtectionSignupPrompt: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional(), - storeTemporaryPartialFormData: _zod.z.record(_zod.z.unknown()).optional() + })).optional() }); },{"zod":9}],70:[function(require,module,exports){ diff --git a/dist/autofill.js b/dist/autofill.js index ecc9b8b76..7b68ab75f 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -13554,7 +13554,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -13806,14 +13806,7 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } -/** - * @extends {DeviceApiCall} - */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { - method = "storeTemporaryPartialFormData"; -} -exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[function(require,module,exports){ "use strict"; diff --git a/src/deviceApiCalls/__generated__/deviceApiCalls.js b/src/deviceApiCalls/__generated__/deviceApiCalls.js index 3411a3d51..e0a61b739 100644 --- a/src/deviceApiCalls/__generated__/deviceApiCalls.js +++ b/src/deviceApiCalls/__generated__/deviceApiCalls.js @@ -243,10 +243,4 @@ export class ShowInContextEmailProtectionSignupPromptCall extends DeviceApiCall method = "ShowInContextEmailProtectionSignupPrompt" id = "ShowInContextEmailProtectionSignupPromptResponse" resultValidator = showInContextEmailProtectionSignupPromptSchema -} -/** - * @extends {DeviceApiCall} - */ -export class StoreTemporaryPartialFormDataCall extends DeviceApiCall { - method = "storeTemporaryPartialFormData" } \ No newline at end of file diff --git a/src/deviceApiCalls/__generated__/validators-ts.ts b/src/deviceApiCalls/__generated__/validators-ts.ts index c759e2481..23787cf7c 100644 --- a/src/deviceApiCalls/__generated__/validators-ts.ts +++ b/src/deviceApiCalls/__generated__/validators-ts.ts @@ -248,12 +248,6 @@ export interface API { resultValidator?: ShowInContextEmailProtectionSignupPrompt; [k: string]: unknown; }; - /** - * Used to store temporary partial form data (username or email) to supplement the signup completion at a later time - */ - storeTemporaryPartialFormData?: { - [k: string]: unknown; - }; } /** * Parameters for the addDebugFlag method diff --git a/src/deviceApiCalls/__generated__/validators.zod.js b/src/deviceApiCalls/__generated__/validators.zod.js index 0ca4538d5..400f8797a 100644 --- a/src/deviceApiCalls/__generated__/validators.zod.js +++ b/src/deviceApiCalls/__generated__/validators.zod.js @@ -435,6 +435,5 @@ export const apiSchema = z.object({ ShowInContextEmailProtectionSignupPrompt: z.record(z.unknown()).and(z.object({ id: z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional(), - storeTemporaryPartialFormData: z.record(z.unknown()).optional() + })).optional() }); diff --git a/src/deviceApiCalls/api.json b/src/deviceApiCalls/api.json index 2137bbaa4..268380bb1 100644 --- a/src/deviceApiCalls/api.json +++ b/src/deviceApiCalls/api.json @@ -313,13 +313,6 @@ "$ref": "./schemas/showInContextEmailProtectionSignupPrompt.result.json" } } - }, - "storeTemporaryPartialFormData": { - "type": "object", - "description": "Used to store temporary partial form data (username or email) to supplement the signup completion at a later time", - "paramsValidator": { - "$ref": "./schemas/storeTemporaryPartialFormData.params.json" - } } } } \ No newline at end of file diff --git a/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json b/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json deleted file mode 100644 index 7d129165e..000000000 --- a/src/deviceApiCalls/schemas/storeTemporaryPartialFormData.params.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StoreTemporaryPartialFormData", - "type": "object", - "description": "Autofill could send this data at any point. \n\nIt will **not** listen for a response, it's expected that the native side will handle", - "additionalProperties": false, - "properties": { - "username": { - "type": "string", - "additionalProperties": false - }, - "trigger": { - "type": "string", - "enum": [ - "partialFormSave" - ] - } - } -} \ No newline at end of file diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 6d11d7f40..ad2a3e1f5 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -17720,7 +17720,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -17972,14 +17972,7 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } -/** - * @extends {DeviceApiCall} - */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { - method = "storeTemporaryPartialFormData"; -} -exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[function(require,module,exports){ "use strict"; @@ -18387,8 +18380,7 @@ const apiSchema = exports.apiSchema = _zod.z.object({ ShowInContextEmailProtectionSignupPrompt: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("ShowInContextEmailProtectionSignupPromptResponse").optional(), resultValidator: showInContextEmailProtectionSignupPromptSchema.optional() - })).optional(), - storeTemporaryPartialFormData: _zod.z.record(_zod.z.unknown()).optional() + })).optional() }); },{"zod":9}],70:[function(require,module,exports){ diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index ecc9b8b76..7b68ab75f 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -13554,7 +13554,7 @@ const constants = exports.constants = { Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreTemporaryPartialFormDataCall = exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CredentialsImportFlowPermanentlyDismissedCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -13806,14 +13806,7 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC id = "ShowInContextEmailProtectionSignupPromptResponse"; resultValidator = _validatorsZod.showInContextEmailProtectionSignupPromptSchema; } -/** - * @extends {DeviceApiCall} - */ exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -class StoreTemporaryPartialFormDataCall extends _deviceApi.DeviceApiCall { - method = "storeTemporaryPartialFormData"; -} -exports.StoreTemporaryPartialFormDataCall = StoreTemporaryPartialFormDataCall; },{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[function(require,module,exports){ "use strict"; From 664a527094e81075efe6afdb87bb84dd90ea4056 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Tue, 19 Nov 2024 13:03:03 +0100 Subject: [PATCH 04/12] feat: update heuristics for save and update tests --- dist/autofill-debug.js | 25 ++++++--- dist/autofill.js | 25 ++++++--- integration-test/helpers/pages/loginPage.js | 6 ++ .../tests/save-prompts.android.spec.js | 4 +- .../tests/save-prompts.ios.spec.js | 4 +- .../tests/save-prompts.macos.spec.js | 4 +- src/DeviceInterface/InterfacePrototype.js | 5 +- src/Form/Form.js | 3 +- src/Form/Form.test.js | 6 +- src/Form/formatters.js | 22 ++++++-- src/Form/formatters.test.js | 55 +++++++++++-------- src/autofill-utils.js | 3 +- .../Resources/assets/autofill-debug.js | 25 ++++++--- swift-package/Resources/assets/autofill.js | 25 ++++++--- 14 files changed, 137 insertions(+), 75 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index ad2a3e1f5..aa1ee9893 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9284,7 +9284,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isEitherEmailOrPhone = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone); + const trigger = isUsernameOnly || isEitherEmailOrPhone ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10065,7 +10067,8 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - return (0, _formatters.prepareFormValuesForStorage)(formValues); + const hasOnlyOneCredential = this.inputs.credentials.size === 1; + return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); } /** @@ -11889,11 +11892,15 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { +const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { let { credentials } = _ref3; - return Boolean(credentials.password) || Boolean(credentials.username); + if (credentials.password) { + return Boolean(credentials.password); + } else { + return hasOnlyOneCredential && Boolean(credentials.username); + } }; /** @@ -11904,7 +11911,7 @@ const shouldStoreIdentities = _ref4 => { let { identities } = _ref4; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; /** @@ -11934,10 +11941,11 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues + * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = formValues => { +const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { /** @type {Partial} */ let { credentials, @@ -11952,7 +11960,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials **/ // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { + if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; @@ -17416,8 +17424,7 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return true; - // return readDebugSetting('ddg-autofill-debug'); + return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/dist/autofill.js b/dist/autofill.js index 7b68ab75f..040a9d9ef 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5118,7 +5118,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isEitherEmailOrPhone = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone); + const trigger = isUsernameOnly || isEitherEmailOrPhone ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -5899,7 +5901,8 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - return (0, _formatters.prepareFormValuesForStorage)(formValues); + const hasOnlyOneCredential = this.inputs.credentials.size === 1; + return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); } /** @@ -7723,11 +7726,15 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { +const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { let { credentials } = _ref3; - return Boolean(credentials.password) || Boolean(credentials.username); + if (credentials.password) { + return Boolean(credentials.password); + } else { + return hasOnlyOneCredential && Boolean(credentials.username); + } }; /** @@ -7738,7 +7745,7 @@ const shouldStoreIdentities = _ref4 => { let { identities } = _ref4; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; /** @@ -7768,10 +7775,11 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues + * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = formValues => { +const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { /** @type {Partial} */ let { credentials, @@ -7786,7 +7794,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials **/ // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { + if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; @@ -13250,8 +13258,7 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return true; - // return readDebugSetting('ddg-autofill-debug'); + return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/integration-test/helpers/pages/loginPage.js b/integration-test/helpers/pages/loginPage.js index f7e7c95fa..a7121f8d9 100644 --- a/integration-test/helpers/pages/loginPage.js +++ b/integration-test/helpers/pages/loginPage.js @@ -215,6 +215,12 @@ export function loginPage(page, opts = {}) { expect(mockCalls.length).toBe(0); } + async shouldPromptToSave() { + let mockCalls = []; + mockCalls = await mockedCalls(page, { names: ['storeFormData'] }); + expect(mockCalls.length).toBeGreaterThan(0); + } + /** * This is used mostly to avoid false negatives when we check for something _not_ happening. * Basically, you check that a specific call hasn't happened but the rest of the script ran just fine. diff --git a/integration-test/tests/save-prompts.android.spec.js b/integration-test/tests/save-prompts.android.spec.js index bf0e55e24..38095b556 100644 --- a/integration-test/tests/save-prompts.android.spec.js +++ b/integration-test/tests/save-prompts.android.spec.js @@ -81,11 +81,11 @@ test.describe('Android Save prompts', () => { await login.submitPasswordOnlyForm(credentials); await login.assertWasPromptedToSave(credentials); }); - test('with username only (should NOT prompt)', async ({ page }) => { + test('with username only (should prompt)', async ({ page }) => { const { login } = await setup(page); const credentials = { username: '123456' }; await login.submitUsernameOnlyForm(credentials.username); - await login.promptWasNotShown(); + await login.shouldPromptToSave(); }); }); }); diff --git a/integration-test/tests/save-prompts.ios.spec.js b/integration-test/tests/save-prompts.ios.spec.js index d93f95f92..ff0801b35 100644 --- a/integration-test/tests/save-prompts.ios.spec.js +++ b/integration-test/tests/save-prompts.ios.spec.js @@ -105,12 +105,12 @@ test.describe('iOS Save prompts', () => { await login.assertWasPromptedToSave(credentials); }); - test('username only (should NOT prompt)', async ({ page }) => { + test('username only (should prompt)', async ({ page }) => { const login = await setup(page); const credentials = { username: '123456' }; await login.submitUsernameOnlyForm(credentials.username); - await login.shouldNotPromptToSave(); + await login.shouldPromptToSave(); }); }); diff --git a/integration-test/tests/save-prompts.macos.spec.js b/integration-test/tests/save-prompts.macos.spec.js index b78cf3ddb..a4d5e8321 100644 --- a/integration-test/tests/save-prompts.macos.spec.js +++ b/integration-test/tests/save-prompts.macos.spec.js @@ -64,7 +64,7 @@ test.describe('macos', () => { await login.submitPasswordOnlyForm(credentials); await login.assertWasPromptedToSave(credentials); }); - test('username only (should NOT prompt)', async ({ page }) => { + test('username only (should prompt)', async ({ page }) => { // enable in-terminal exceptions await forwardConsoleMessages(page); @@ -76,7 +76,7 @@ test.describe('macos', () => { const login = loginPage(page); await login.navigate(); await login.submitUsernameOnlyForm(credentials.username); - await login.shouldNotPromptToSave(); + await login.shouldPromptToSave(); }); }); }); diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index a37abb3ec..949e0f75f 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -808,7 +808,10 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + const isUsernameOnly = + Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isEitherEmailOrPhone = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone); + const trigger = isUsernameOnly || isEitherEmailOrPhone ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } diff --git a/src/Form/Form.js b/src/Form/Form.js index fb20c6433..bc3acf88e 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -238,7 +238,8 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - return prepareFormValuesForStorage(formValues); + const hasOnlyOneCredential = this.inputs.credentials.size === 1; + return prepareFormValuesForStorage(formValues, hasOnlyOneCredential); } /** diff --git a/src/Form/Form.test.js b/src/Form/Form.test.js index 86e916019..b0890a803 100644 --- a/src/Form/Form.test.js +++ b/src/Form/Form.test.js @@ -276,7 +276,11 @@ describe('Test the form class reading values correctly', () => { `, expHasValues: true, expValues: { - identities: undefined, + identities: { + emailAddress: 'peppapig@email.com', + firstName: 'Peppa', + lastName: 'Pig', + }, creditCards: { cardName: 'Peppa Pig', cardSecurityCode: '123', diff --git a/src/Form/formatters.js b/src/Form/formatters.js index bf4563ec8..52e0fa3c8 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -163,14 +163,25 @@ const getMMAndYYYYFromString = (expiration) => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCredentials = ({ credentials }) => Boolean(credentials.password) || Boolean(credentials.username); +const shouldStoreCredentials = ({ credentials }, hasOnlyOneCredential) => { + if (credentials.password) { + return Boolean(credentials.password); + } else { + return hasOnlyOneCredential && Boolean(credentials.username); + } +}; /** * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreIdentities = ({ identities }) => - Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); +const shouldStoreIdentities = ({ identities }) => { + return ( + Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || + Boolean(identities.emailAddress) || + Boolean(identities.phone) + ); +}; /** * @param {InternalDataStorageObject} credentials @@ -196,9 +207,10 @@ const formatPhoneNumber = (phone) => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues + * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ -const prepareFormValuesForStorage = (formValues) => { +const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { /** @type {Partial} */ let { credentials, identities, creditCards } = formValues; @@ -209,7 +221,7 @@ const prepareFormValuesForStorage = (formValues) => { /** Fixes for credentials **/ // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { + if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; diff --git a/src/Form/formatters.test.js b/src/Form/formatters.test.js index df3b1ed4d..91203144e 100644 --- a/src/Form/formatters.test.js +++ b/src/Form/formatters.test.js @@ -61,36 +61,45 @@ describe('Can strip phone formatting characters', () => { describe('prepareFormValuesForStorage()', () => { describe('handling credentials', () => { - it('rejects for username only', () => { - const values = prepareFormValuesForStorage({ - credentials: { username: 'dax@example.com' }, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }); + it.skip('rejects for username only', () => { + const values = prepareFormValuesForStorage( + { + credentials: { username: 'dax@example.com' }, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }, + false, + ); expect(values.credentials).toBeUndefined(); }); it('accepts password only', () => { - const values = prepareFormValuesForStorage({ - // @ts-ignore - credentials: { password: '123456' }, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }); + const values = prepareFormValuesForStorage( + { + // @ts-ignore + credentials: { password: '123456' }, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }, + false, + ); expect(values.credentials?.password).toBe('123456'); }); it('accepts username+password', () => { const inputCredentials = { username: 'dax@example.com', password: '123456' }; - const values = prepareFormValuesForStorage({ - credentials: inputCredentials, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }); + const values = prepareFormValuesForStorage( + { + credentials: inputCredentials, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }, + false, + ); expect(values.credentials).toEqual(inputCredentials); }); }); diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 09255eb0a..5a4984052 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -426,8 +426,7 @@ const wasAutofilledByChrome = (input) => { * @returns {boolean} */ function shouldLog() { - return true; - // return readDebugSetting('ddg-autofill-debug'); + return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index ad2a3e1f5..aa1ee9893 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9284,7 +9284,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isEitherEmailOrPhone = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone); + const trigger = isUsernameOnly || isEitherEmailOrPhone ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10065,7 +10067,8 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - return (0, _formatters.prepareFormValuesForStorage)(formValues); + const hasOnlyOneCredential = this.inputs.credentials.size === 1; + return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); } /** @@ -11889,11 +11892,15 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { +const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { let { credentials } = _ref3; - return Boolean(credentials.password) || Boolean(credentials.username); + if (credentials.password) { + return Boolean(credentials.password); + } else { + return hasOnlyOneCredential && Boolean(credentials.username); + } }; /** @@ -11904,7 +11911,7 @@ const shouldStoreIdentities = _ref4 => { let { identities } = _ref4; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; /** @@ -11934,10 +11941,11 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues + * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = formValues => { +const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { /** @type {Partial} */ let { credentials, @@ -11952,7 +11960,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials **/ // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { + if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; @@ -17416,8 +17424,7 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return true; - // return readDebugSetting('ddg-autofill-debug'); + return readDebugSetting('ddg-autofill-debug'); } /** diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 7b68ab75f..040a9d9ef 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5118,7 +5118,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const trigger = formData.credentials?.username && !formData.credentials?.password ? 'partialSave' : 'formSubmission'; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isEitherEmailOrPhone = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone); + const trigger = isUsernameOnly || isEitherEmailOrPhone ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -5899,7 +5901,8 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - return (0, _formatters.prepareFormValuesForStorage)(formValues); + const hasOnlyOneCredential = this.inputs.credentials.size === 1; + return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); } /** @@ -7723,11 +7726,15 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = _ref3 => { +const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { let { credentials } = _ref3; - return Boolean(credentials.password) || Boolean(credentials.username); + if (credentials.password) { + return Boolean(credentials.password); + } else { + return hasOnlyOneCredential && Boolean(credentials.username); + } }; /** @@ -7738,7 +7745,7 @@ const shouldStoreIdentities = _ref4 => { let { identities } = _ref4; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; /** @@ -7768,10 +7775,11 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues + * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = formValues => { +const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { /** @type {Partial} */ let { credentials, @@ -7786,7 +7794,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials **/ // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues)) { + if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; @@ -13250,8 +13258,7 @@ const wasAutofilledByChrome = input => { */ exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { - return true; - // return readDebugSetting('ddg-autofill-debug'); + return readDebugSetting('ddg-autofill-debug'); } /** From d8c7d83bd0188e2ca7c9f77c4681e0476736d140 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Fri, 22 Nov 2024 17:16:57 +0100 Subject: [PATCH 05/12] feat: prompt when autofilled --- dist/autofill-debug.js | 5 ++++- dist/autofill.js | 5 ++++- src/Form/Form.js | 7 ++++++- swift-package/Resources/assets/autofill-debug.js | 5 ++++- swift-package/Resources/assets/autofill.js | 5 ++++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 7750b022a..23a3f8088 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -10694,8 +10694,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); + const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - if (areAllFormValuesKnown) { + // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. + if (areAllFormValuesKnown && !hasOnlyOneCredential) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value diff --git a/dist/autofill.js b/dist/autofill.js index 350b27947..471189fca 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -6528,8 +6528,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); + const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - if (areAllFormValuesKnown) { + // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. + if (areAllFormValuesKnown && !hasOnlyOneCredential) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value diff --git a/src/Form/Form.js b/src/Form/Form.js index bc3acf88e..9f81b23c0 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -902,10 +902,15 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); + const hasOnlyOneCredential = + (Boolean(formValues.credentials?.username) && !formValues.credentials?.password) || + (Boolean(formValues.credentials?.password) && !formValues.credentials?.username); const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); - if (areAllFormValuesKnown) { + // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. + if (areAllFormValuesKnown && !hasOnlyOneCredential) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 7750b022a..23a3f8088 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -10694,8 +10694,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); + const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - if (areAllFormValuesKnown) { + // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. + if (areAllFormValuesKnown && !hasOnlyOneCredential) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 350b27947..471189fca 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -6528,8 +6528,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); + const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - if (areAllFormValuesKnown) { + // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. + if (areAllFormValuesKnown && !hasOnlyOneCredential) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value From 40ccc9677368e4786ed3c3caa0ca53502dfbb39f Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Tue, 3 Dec 2024 15:46:12 +0100 Subject: [PATCH 06/12] chore: rebuild assets --- dist/autofill-debug.js | 457 +++++++++++++----- dist/autofill.js | 26 +- src/Form/Form.js | 11 +- .../__generated__/validators.zod.js | 134 ++--- .../Resources/assets/autofill-debug.js | 457 +++++++++++++----- swift-package/Resources/assets/autofill.js | 26 +- 6 files changed, 758 insertions(+), 353 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 111f88938..4980f6640 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -138,6 +138,11 @@ class ZodError extends Error { processError(this); return fieldErrors; } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } toString() { return this.message; } @@ -267,6 +272,13 @@ const makeIssue = params => { ...issueData, path: fullPath }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } let errorMessage = ""; const maps = errorMaps.filter(m => !!m).slice().reverse(); for (const map of maps) { @@ -278,17 +290,18 @@ const makeIssue = params => { return { ...issueData, path: fullPath, - message: issueData.message || errorMessage + message: errorMessage }; }; exports.makeIssue = makeIssue; exports.EMPTY_PATH = []; function addIssueToContext(ctx, issueData) { + const overrideMap = (0, errors_1.getErrorMap)(); const issue = (0, exports.makeIssue)({ issueData: issueData, data: ctx.data, path: ctx.path, - errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, (0, errors_1.getErrorMap)(), en_1.default // then global default map + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, overrideMap, overrideMap === en_1.default ? undefined : en_1.default // then global default map ].filter(x => !!x) }); ctx.common.issues.push(issue); @@ -319,9 +332,11 @@ class ParseStatus { static async mergeObjectAsync(status, pairs) { const syncPairs = []; for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; syncPairs.push({ - key: await pair.key, - value: await pair.value + key, + value }); } return ParseStatus.mergeObjectSync(status, syncPairs); @@ -632,11 +647,23 @@ exports.default = errorMap; },{"../ZodError":2,"../helpers/util":8}],11:[function(require,module,exports){ "use strict"; +var __classPrivateFieldGet = void 0 && (void 0).__classPrivateFieldGet || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __classPrivateFieldSet = void 0 && (void 0).__classPrivateFieldSet || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; +}; +var _ZodEnum_cache, _ZodNativeEnum_cache; Object.defineProperty(exports, "__esModule", { value: true }); -exports.date = exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.ZodType = void 0; -exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = void 0; +exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.datetimeRegex = exports.ZodType = void 0; +exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = exports.date = void 0; const errors_1 = require("./errors"); const errorUtil_1 = require("./helpers/errorUtil"); const parseUtil_1 = require("./helpers/parseUtil"); @@ -698,16 +725,25 @@ function processCreateParams(params) { description }; const customMap = (iss, ctx) => { - if (iss.code !== "invalid_type") return { - message: ctx.defaultError - }; + var _a, _b; + const { + message + } = params; + if (iss.code === "invalid_enum_value") { + return { + message: message !== null && message !== void 0 ? message : ctx.defaultError + }; + } if (typeof ctx.data === "undefined") { return { - message: required_error !== null && required_error !== void 0 ? required_error : ctx.defaultError + message: (_a = message !== null && message !== void 0 ? message : required_error) !== null && _a !== void 0 ? _a : ctx.defaultError }; } + if (iss.code !== "invalid_type") return { + message: ctx.defaultError + }; return { - message: invalid_type_error !== null && invalid_type_error !== void 0 ? invalid_type_error : ctx.defaultError + message: (_b = message !== null && message !== void 0 ? message : invalid_type_error) !== null && _b !== void 0 ? _b : ctx.defaultError }; }; return { @@ -977,11 +1013,13 @@ exports.ZodType = ZodType; exports.Schema = ZodType; exports.ZodSchema = ZodType; const cuidRegex = /^c[^\s-]{8,}$/i; -const cuid2Regex = /^[a-z][a-z0-9]*$/; +const cuid2Regex = /^[0-9a-z]+$/; const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; // const uuidRegex = // /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; // from https://stackoverflow.com/a/46181/1550155 // old version: too slow, didn't support unicode // const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; @@ -994,36 +1032,47 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA- // /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; // const emailRegex = // /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; -const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; // const emailRegex = // /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; // from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; let emojiRegex; -const ipv4Regex = /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; -// Adapted from https://stackoverflow.com/a/3143231 -const datetimeRegex = args => { +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + // let regex = `\\d{2}:\\d{2}:\\d{2}`; + let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`; if (args.precision) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}Z$`); - } - } else if (args.precision === 0) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$`); - } - } else { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`); - } + regex = `${regex}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + regex = `${regex}(\\.\\d+)?`; } -}; + return regex; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +exports.datetimeRegex = datetimeRegex; function isValidIP(ip, version) { if ((version === "v4" || !version) && ipv4Regex.test(ip)) { return true; @@ -1045,10 +1094,7 @@ class ZodString extends ZodType { code: ZodError_1.ZodIssueCode.invalid_type, expected: util_1.ZodParsedType.string, received: ctx.parsedType - } - // - ); - + }); return parseUtil_1.INVALID; } const status = new parseUtil_1.ParseStatus(); @@ -1139,6 +1185,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "nanoid", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "cuid") { if (!cuidRegex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1247,6 +1303,38 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "duration", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "ip") { if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1257,6 +1345,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "base64", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else { util_1.util.assertNever(check); } @@ -1303,6 +1401,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + nanoid(message) { + return this._addCheck({ + kind: "nanoid", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } cuid(message) { return this._addCheck({ kind: "cuid", @@ -1321,6 +1425,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + base64(message) { + return this._addCheck({ + kind: "base64", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } ip(options) { return this._addCheck({ kind: "ip", @@ -1328,12 +1438,13 @@ class ZodString extends ZodType { }); } datetime(options) { - var _a; + var _a, _b; if (typeof options === "string") { return this._addCheck({ kind: "datetime", precision: null, offset: false, + local: false, message: options }); } @@ -1341,9 +1452,36 @@ class ZodString extends ZodType { kind: "datetime", precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, offset: (_a = options === null || options === void 0 ? void 0 : options.offset) !== null && _a !== void 0 ? _a : false, + local: (_b = options === null || options === void 0 ? void 0 : options.local) !== null && _b !== void 0 ? _b : false, + ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) + }); + } + date(message) { + return this._addCheck({ + kind: "date", + message + }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) }); } + duration(message) { + return this._addCheck({ + kind: "duration", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } regex(regex, message) { return this._addCheck({ kind: "regex", @@ -1428,6 +1566,15 @@ class ZodString extends ZodType { get isDatetime() { return !!this._def.checks.find(ch => ch.kind === "datetime"); } + get isDate() { + return !!this._def.checks.find(ch => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find(ch => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find(ch => ch.kind === "duration"); + } get isEmail() { return !!this._def.checks.find(ch => ch.kind === "email"); } @@ -1440,6 +1587,9 @@ class ZodString extends ZodType { get isUUID() { return !!this._def.checks.find(ch => ch.kind === "uuid"); } + get isNANOID() { + return !!this._def.checks.find(ch => ch.kind === "nanoid"); + } get isCUID() { return !!this._def.checks.find(ch => ch.kind === "cuid"); } @@ -1452,6 +1602,9 @@ class ZodString extends ZodType { get isIP() { return !!this._def.checks.find(ch => ch.kind === "ip"); } + get isBase64() { + return !!this._def.checks.find(ch => ch.kind === "base64"); + } get minLength() { let min = null; for (const ch of this._def.checks) { @@ -2442,9 +2595,10 @@ class ZodObject extends ZodType { const syncPairs = []; for (const pair of pairs) { const key = await pair.key; + const value = await pair.value; syncPairs.push({ key, - value: await pair.value, + value, alwaysSet: pair.alwaysSet }); } @@ -2814,15 +2968,25 @@ const getDiscriminator = type => { return type.options; } else if (type instanceof ZodNativeEnum) { // eslint-disable-next-line ban/ban - return Object.keys(type.enum); + return util_1.util.objectValues(type.enum); } else if (type instanceof ZodDefault) { return getDiscriminator(type._def.innerType); } else if (type instanceof ZodUndefined) { return [undefined]; } else if (type instanceof ZodNull) { return [null]; + } else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); } else { - return null; + return []; } }; class ZodDiscriminatedUnion extends ZodType { @@ -2886,7 +3050,7 @@ class ZodDiscriminatedUnion extends ZodType { // try { for (const type of options) { const discriminatorValues = getDiscriminator(type.shape[discriminator]); - if (!discriminatorValues) { + if (!discriminatorValues.length) { throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); } for (const value of discriminatorValues) { @@ -3123,7 +3287,8 @@ class ZodRecord extends ZodType { for (const key in ctx.data) { pairs.push({ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), - value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)) + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data }); } if (ctx.common.async) { @@ -3511,6 +3676,10 @@ function createZodEnum(values, params) { }); } class ZodEnum extends ZodType { + constructor() { + super(...arguments); + _ZodEnum_cache.set(this, void 0); + } _parse(input) { if (typeof input.data !== "string") { const ctx = this._getOrReturnCtx(input); @@ -3522,7 +3691,10 @@ class ZodEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (this._def.values.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodEnum_cache, new Set(this._def.values), "f"); + } + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f").has(input.data)) { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; (0, parseUtil_1.addIssueToContext)(ctx, { @@ -3559,15 +3731,28 @@ class ZodEnum extends ZodType { return enumValues; } extract(values) { - return ZodEnum.create(values); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(values, { + ...this._def, + ...newDef + }); } exclude(values) { - return ZodEnum.create(this.options.filter(opt => !values.includes(opt))); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(this.options.filter(opt => !values.includes(opt)), { + ...this._def, + ...newDef + }); } } exports.ZodEnum = ZodEnum; +_ZodEnum_cache = new WeakMap(); ZodEnum.create = createZodEnum; class ZodNativeEnum extends ZodType { + constructor() { + super(...arguments); + _ZodNativeEnum_cache.set(this, void 0); + } _parse(input) { const nativeEnumValues = util_1.util.getValidEnumValues(this._def.values); const ctx = this._getOrReturnCtx(input); @@ -3580,7 +3765,10 @@ class ZodNativeEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (nativeEnumValues.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodNativeEnum_cache, new Set(util_1.util.getValidEnumValues(this._def.values)), "f"); + } + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f").has(input.data)) { const expectedValues = util_1.util.objectValues(nativeEnumValues); (0, parseUtil_1.addIssueToContext)(ctx, { received: ctx.data, @@ -3596,6 +3784,7 @@ class ZodNativeEnum extends ZodType { } } exports.ZodNativeEnum = ZodNativeEnum; +_ZodNativeEnum_cache = new WeakMap(); ZodNativeEnum.create = (values, params) => { return new ZodNativeEnum({ values: values, @@ -3665,32 +3854,34 @@ class ZodEffects extends ZodType { checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); if (effect.type === "preprocess") { const processed = effect.transform(ctx.data, checkCtx); - if (ctx.common.issues.length) { - return { - status: "dirty", - value: ctx.data - }; - } if (ctx.common.async) { - return Promise.resolve(processed).then(processed => { - return this._def.schema._parseAsync({ + return Promise.resolve(processed).then(async processed => { + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = await this._def.schema._parseAsync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; }); } else { - return this._def.schema._parseSync({ + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = this._def.schema._parseSync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; } } if (effect.type === "refinement") { - const executeRefinement = (acc - // effect: RefinementEffect - ) => { + const executeRefinement = acc => { const result = effect.refinement(acc, checkCtx); if (ctx.common.async) { return Promise.resolve(result); @@ -4013,10 +4204,16 @@ exports.ZodPipeline = ZodPipeline; class ZodReadonly extends ZodType { _parse(input) { const result = this._def.innerType._parse(input); - if ((0, parseUtil_1.isValid)(result)) { - result.value = Object.freeze(result.value); - } - return result; + const freeze = data => { + if ((0, parseUtil_1.isValid)(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return (0, parseUtil_1.isAsync)(result) ? result.then(data => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; } } exports.ZodReadonly = ZodReadonly; @@ -4027,7 +4224,7 @@ ZodReadonly.create = (type, params) => { ...processCreateParams(params) }); }; -const custom = function (check) { +function custom(check) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let /** @@ -4059,7 +4256,7 @@ const custom = function (check) { } }); return ZodAny.create(); -}; +} exports.custom = custom; exports.late = { object: ZodObject.lazycreate @@ -4113,7 +4310,7 @@ cls) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { message: `Input not instance of ${cls.name}` }; - return (0, exports.custom)(data => data instanceof cls, params); + return custom(data => data instanceof cls, params); }; exports.instanceof = instanceOfType; const stringType = ZodString.create; @@ -10685,11 +10882,13 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; + const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredential) { + if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value @@ -18190,6 +18389,26 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillInitDataResponse").optional(), + success: _zod.z.object({ + credentials: _zod.z.array(credentialsSchema), + identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), + creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), + serializedInputContext: _zod.z.string() + }).optional(), + error: genericErrorSchema.optional() +}); +const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillCredentialsResponse").optional(), + success: _zod.z.object({ + id: _zod.z.string().optional(), + autogenerated: _zod.z.boolean().optional(), + username: _zod.z.string(), + password: _zod.z.string().optional() + }).optional(), + error: genericErrorSchema.optional() +}); const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18222,6 +18441,11 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ + status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), + credentials: _zod.z.array(credentialsSchema), + availableInputTypes: availableInputTypes1Schema +}); const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({ inputType_credentials: _zod.z.boolean().optional(), inputType_identities: _zod.z.boolean().optional(), @@ -18234,55 +18458,6 @@ const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod third_party_credentials_provider: _zod.z.boolean().optional(), unknown_username_categorization: _zod.z.boolean().optional() }); -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ - generatedPassword: generatedPasswordSchema.optional(), - inputType: _zod.z.string(), - mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), - subType: _zod.z.string(), - trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), - serializedInputContext: _zod.z.string().optional(), - triggerContext: triggerContextSchema.optional() -}); -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillDataResponse").optional(), - success: _zod.z.object({ - credentials: credentialsSchema.optional(), - action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) - }).optional(), - error: genericErrorSchema.optional() -}); -const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ - credentials: outgoingCredentialsSchema.optional(), - trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() -}); -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ - type: _zod.z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypesSchema, - error: genericErrorSchema.optional() -}); -const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillInitDataResponse").optional(), - success: _zod.z.object({ - credentials: _zod.z.array(credentialsSchema), - identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), - creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), - serializedInputContext: _zod.z.string() - }).optional(), - error: genericErrorSchema.optional() -}); -const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillCredentialsResponse").optional(), - success: _zod.z.object({ - id: _zod.z.string().optional(), - autogenerated: _zod.z.boolean().optional(), - username: _zod.z.string(), - password: _zod.z.string().optional() - }).optional(), - error: genericErrorSchema.optional() -}); -const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ - featureToggles: autofillFeatureTogglesSchema -}); const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = _zod.z.object({ success: _zod.z.boolean().optional(), error: genericErrorSchema.optional() @@ -18317,19 +18492,30 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection }).optional(), error: genericErrorSchema.optional() }); -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ - contentScope: contentScopeSchema, - userUnprotectedDomains: _zod.z.array(_zod.z.string()), - userPreferences: userPreferencesSchema +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ + generatedPassword: generatedPasswordSchema.optional(), + inputType: _zod.z.string(), + mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), + subType: _zod.z.string(), + trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), + serializedInputContext: _zod.z.string().optional(), + triggerContext: triggerContextSchema.optional() }); -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ - status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), - credentials: _zod.z.array(credentialsSchema), - availableInputTypes: availableInputTypes1Schema +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillDataResponse").optional(), + success: _zod.z.object({ + credentials: credentialsSchema.optional(), + action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) + }).optional(), + error: genericErrorSchema.optional() }); -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ - type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), - success: runtimeConfigurationSchema.optional(), +const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ + credentials: outgoingCredentialsSchema.optional(), + trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() +}); +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ + type: _zod.z.literal("getAvailableInputTypesResponse").optional(), + success: availableInputTypesSchema, error: genericErrorSchema.optional() }); const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({ @@ -18342,6 +18528,19 @@ const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProvi success: providerStatusUpdatedSchema, error: genericErrorSchema.optional() }); +const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ + featureToggles: autofillFeatureTogglesSchema +}); +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ + contentScope: contentScopeSchema, + userUnprotectedDomains: _zod.z.array(_zod.z.string()), + userPreferences: userPreferencesSchema +}); +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ + type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), + success: runtimeConfigurationSchema.optional(), + error: genericErrorSchema.optional() +}); const apiSchema = exports.apiSchema = _zod.z.object({ addDebugFlag: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ paramsValidator: addDebugFlagParamsSchema.optional() diff --git a/dist/autofill.js b/dist/autofill.js index 6063f2bd4..4104fb4b9 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -6519,11 +6519,13 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; + const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredential) { + if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value @@ -13894,25 +13896,25 @@ const contentScopeSchema = exports.contentScopeSchema = null; const userPreferencesSchema = exports.userPreferencesSchema = null; const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; const availableInputTypesSchema = exports.availableInputTypesSchema = null; -const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; -const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; -const storeFormDataSchema = exports.storeFormDataSchema = null; -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = null; const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = null; -const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; +const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = null; const emailProtectionGetUserDataResultSchema = exports.emailProtectionGetUserDataResultSchema = null; const emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = null; const emailProtectionGetAddressesResultSchema = exports.emailProtectionGetAddressesResultSchema = null; const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = null; -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; +const storeFormDataSchema = exports.storeFormDataSchema = null; +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null; const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null; +const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; const apiSchema = exports.apiSchema = null; },{}],60:[function(require,module,exports){ diff --git a/src/Form/Form.js b/src/Form/Form.js index 1ae9775e1..d999b08e2 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -896,15 +896,18 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasOnlyOneCredential = - (Boolean(formValues.credentials?.username) && !formValues.credentials?.password) || - (Boolean(formValues.credentials?.password) && !formValues.credentials?.username); + const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + + const hasOnlyOneCredentialOrEmail = + Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || + (hasOnlyEmail && hasNoCredentialsData); const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredential) { + if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value diff --git a/src/deviceApiCalls/__generated__/validators.zod.js b/src/deviceApiCalls/__generated__/validators.zod.js index 400f8797a..87ae05418 100644 --- a/src/deviceApiCalls/__generated__/validators.zod.js +++ b/src/deviceApiCalls/__generated__/validators.zod.js @@ -166,6 +166,28 @@ export const availableInputTypesSchema = z.object({ credentialsImport: z.boolean().optional() }); +export const getAutofillInitDataResponseSchema = z.object({ + type: z.literal("getAutofillInitDataResponse").optional(), + success: z.object({ + credentials: z.array(credentialsSchema), + identities: z.array(z.record(z.unknown())), + creditCards: z.array(z.record(z.unknown())), + serializedInputContext: z.string() + }).optional(), + error: genericErrorSchema.optional() +}); + +export const getAutofillCredentialsResultSchema = z.object({ + type: z.literal("getAutofillCredentialsResponse").optional(), + success: z.object({ + id: z.string().optional(), + autogenerated: z.boolean().optional(), + username: z.string(), + password: z.string().optional() + }).optional(), + error: genericErrorSchema.optional() +}); + export const availableInputTypes1Schema = z.object({ credentials: z.object({ username: z.boolean().optional(), @@ -199,6 +221,12 @@ export const availableInputTypes1Schema = z.object({ credentialsImport: z.boolean().optional() }); +export const providerStatusUpdatedSchema = z.object({ + status: z.union([z.literal("locked"), z.literal("unlocked")]), + credentials: z.array(credentialsSchema), + availableInputTypes: availableInputTypes1Schema +}); + export const autofillFeatureTogglesSchema = z.object({ inputType_credentials: z.boolean().optional(), inputType_identities: z.boolean().optional(), @@ -212,62 +240,6 @@ export const autofillFeatureTogglesSchema = z.object({ unknown_username_categorization: z.boolean().optional() }); -export const getAutofillDataRequestSchema = z.object({ - generatedPassword: generatedPasswordSchema.optional(), - inputType: z.string(), - mainType: z.union([z.literal("credentials"), z.literal("identities"), z.literal("creditCards")]), - subType: z.string(), - trigger: z.union([z.literal("userInitiated"), z.literal("autoprompt"), z.literal("postSignup")]).optional(), - serializedInputContext: z.string().optional(), - triggerContext: triggerContextSchema.optional() -}); - -export const getAutofillDataResponseSchema = z.object({ - type: z.literal("getAutofillDataResponse").optional(), - success: z.object({ - credentials: credentialsSchema.optional(), - action: z.union([z.literal("fill"), z.literal("focus"), z.literal("none"), z.literal("refreshAvailableInputTypes"), z.literal("acceptGeneratedPassword"), z.literal("rejectGeneratedPassword")]) - }).optional(), - error: genericErrorSchema.optional() -}); - -export const storeFormDataSchema = z.object({ - credentials: outgoingCredentialsSchema.optional(), - trigger: z.union([z.literal("partialSave"), z.literal("formSubmission"), z.literal("passwordGeneration"), z.literal("emailProtection")]).optional() -}); - -export const getAvailableInputTypesResultSchema = z.object({ - type: z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypesSchema, - error: genericErrorSchema.optional() -}); - -export const getAutofillInitDataResponseSchema = z.object({ - type: z.literal("getAutofillInitDataResponse").optional(), - success: z.object({ - credentials: z.array(credentialsSchema), - identities: z.array(z.record(z.unknown())), - creditCards: z.array(z.record(z.unknown())), - serializedInputContext: z.string() - }).optional(), - error: genericErrorSchema.optional() -}); - -export const getAutofillCredentialsResultSchema = z.object({ - type: z.literal("getAutofillCredentialsResponse").optional(), - success: z.object({ - id: z.string().optional(), - autogenerated: z.boolean().optional(), - username: z.string(), - password: z.string().optional() - }).optional(), - error: genericErrorSchema.optional() -}); - -export const autofillSettingsSchema = z.object({ - featureToggles: autofillFeatureTogglesSchema -}); - export const emailProtectionGetIsLoggedInResultSchema = z.object({ success: z.boolean().optional(), error: genericErrorSchema.optional() @@ -307,21 +279,33 @@ export const emailProtectionRefreshPrivateAddressResultSchema = z.object({ error: genericErrorSchema.optional() }); -export const runtimeConfigurationSchema = z.object({ - contentScope: contentScopeSchema, - userUnprotectedDomains: z.array(z.string()), - userPreferences: userPreferencesSchema +export const getAutofillDataRequestSchema = z.object({ + generatedPassword: generatedPasswordSchema.optional(), + inputType: z.string(), + mainType: z.union([z.literal("credentials"), z.literal("identities"), z.literal("creditCards")]), + subType: z.string(), + trigger: z.union([z.literal("userInitiated"), z.literal("autoprompt"), z.literal("postSignup")]).optional(), + serializedInputContext: z.string().optional(), + triggerContext: triggerContextSchema.optional() }); -export const providerStatusUpdatedSchema = z.object({ - status: z.union([z.literal("locked"), z.literal("unlocked")]), - credentials: z.array(credentialsSchema), - availableInputTypes: availableInputTypes1Schema +export const getAutofillDataResponseSchema = z.object({ + type: z.literal("getAutofillDataResponse").optional(), + success: z.object({ + credentials: credentialsSchema.optional(), + action: z.union([z.literal("fill"), z.literal("focus"), z.literal("none"), z.literal("refreshAvailableInputTypes"), z.literal("acceptGeneratedPassword"), z.literal("rejectGeneratedPassword")]) + }).optional(), + error: genericErrorSchema.optional() }); -export const getRuntimeConfigurationResponseSchema = z.object({ - type: z.literal("getRuntimeConfigurationResponse").optional(), - success: runtimeConfigurationSchema.optional(), +export const storeFormDataSchema = z.object({ + credentials: outgoingCredentialsSchema.optional(), + trigger: z.union([z.literal("partialSave"), z.literal("formSubmission"), z.literal("passwordGeneration"), z.literal("emailProtection")]).optional() +}); + +export const getAvailableInputTypesResultSchema = z.object({ + type: z.literal("getAvailableInputTypesResponse").optional(), + success: availableInputTypesSchema, error: genericErrorSchema.optional() }); @@ -337,6 +321,22 @@ export const checkCredentialsProviderStatusResultSchema = z.object({ error: genericErrorSchema.optional() }); +export const autofillSettingsSchema = z.object({ + featureToggles: autofillFeatureTogglesSchema +}); + +export const runtimeConfigurationSchema = z.object({ + contentScope: contentScopeSchema, + userUnprotectedDomains: z.array(z.string()), + userPreferences: userPreferencesSchema +}); + +export const getRuntimeConfigurationResponseSchema = z.object({ + type: z.literal("getRuntimeConfigurationResponse").optional(), + success: runtimeConfigurationSchema.optional(), + error: genericErrorSchema.optional() +}); + export const apiSchema = z.object({ addDebugFlag: z.record(z.unknown()).and(z.object({ paramsValidator: addDebugFlagParamsSchema.optional() diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 111f88938..4980f6640 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -138,6 +138,11 @@ class ZodError extends Error { processError(this); return fieldErrors; } + static assert(value) { + if (!(value instanceof ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } toString() { return this.message; } @@ -267,6 +272,13 @@ const makeIssue = params => { ...issueData, path: fullPath }; + if (issueData.message !== undefined) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } let errorMessage = ""; const maps = errorMaps.filter(m => !!m).slice().reverse(); for (const map of maps) { @@ -278,17 +290,18 @@ const makeIssue = params => { return { ...issueData, path: fullPath, - message: issueData.message || errorMessage + message: errorMessage }; }; exports.makeIssue = makeIssue; exports.EMPTY_PATH = []; function addIssueToContext(ctx, issueData) { + const overrideMap = (0, errors_1.getErrorMap)(); const issue = (0, exports.makeIssue)({ issueData: issueData, data: ctx.data, path: ctx.path, - errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, (0, errors_1.getErrorMap)(), en_1.default // then global default map + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, overrideMap, overrideMap === en_1.default ? undefined : en_1.default // then global default map ].filter(x => !!x) }); ctx.common.issues.push(issue); @@ -319,9 +332,11 @@ class ParseStatus { static async mergeObjectAsync(status, pairs) { const syncPairs = []; for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; syncPairs.push({ - key: await pair.key, - value: await pair.value + key, + value }); } return ParseStatus.mergeObjectSync(status, syncPairs); @@ -632,11 +647,23 @@ exports.default = errorMap; },{"../ZodError":2,"../helpers/util":8}],11:[function(require,module,exports){ "use strict"; +var __classPrivateFieldGet = void 0 && (void 0).__classPrivateFieldGet || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var __classPrivateFieldSet = void 0 && (void 0).__classPrivateFieldSet || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; +}; +var _ZodEnum_cache, _ZodNativeEnum_cache; Object.defineProperty(exports, "__esModule", { value: true }); -exports.date = exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.ZodType = void 0; -exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = void 0; +exports.boolean = exports.bigint = exports.array = exports.any = exports.coerce = exports.ZodFirstPartyTypeKind = exports.late = exports.ZodSchema = exports.Schema = exports.custom = exports.ZodReadonly = exports.ZodPipeline = exports.ZodBranded = exports.BRAND = exports.ZodNaN = exports.ZodCatch = exports.ZodDefault = exports.ZodNullable = exports.ZodOptional = exports.ZodTransformer = exports.ZodEffects = exports.ZodPromise = exports.ZodNativeEnum = exports.ZodEnum = exports.ZodLiteral = exports.ZodLazy = exports.ZodFunction = exports.ZodSet = exports.ZodMap = exports.ZodRecord = exports.ZodTuple = exports.ZodIntersection = exports.ZodDiscriminatedUnion = exports.ZodUnion = exports.ZodObject = exports.ZodArray = exports.ZodVoid = exports.ZodNever = exports.ZodUnknown = exports.ZodAny = exports.ZodNull = exports.ZodUndefined = exports.ZodSymbol = exports.ZodDate = exports.ZodBoolean = exports.ZodBigInt = exports.ZodNumber = exports.ZodString = exports.datetimeRegex = exports.ZodType = void 0; +exports.NEVER = exports.void = exports.unknown = exports.union = exports.undefined = exports.tuple = exports.transformer = exports.symbol = exports.string = exports.strictObject = exports.set = exports.record = exports.promise = exports.preprocess = exports.pipeline = exports.ostring = exports.optional = exports.onumber = exports.oboolean = exports.object = exports.number = exports.nullable = exports.null = exports.never = exports.nativeEnum = exports.nan = exports.map = exports.literal = exports.lazy = exports.intersection = exports.instanceof = exports.function = exports.enum = exports.effect = exports.discriminatedUnion = exports.date = void 0; const errors_1 = require("./errors"); const errorUtil_1 = require("./helpers/errorUtil"); const parseUtil_1 = require("./helpers/parseUtil"); @@ -698,16 +725,25 @@ function processCreateParams(params) { description }; const customMap = (iss, ctx) => { - if (iss.code !== "invalid_type") return { - message: ctx.defaultError - }; + var _a, _b; + const { + message + } = params; + if (iss.code === "invalid_enum_value") { + return { + message: message !== null && message !== void 0 ? message : ctx.defaultError + }; + } if (typeof ctx.data === "undefined") { return { - message: required_error !== null && required_error !== void 0 ? required_error : ctx.defaultError + message: (_a = message !== null && message !== void 0 ? message : required_error) !== null && _a !== void 0 ? _a : ctx.defaultError }; } + if (iss.code !== "invalid_type") return { + message: ctx.defaultError + }; return { - message: invalid_type_error !== null && invalid_type_error !== void 0 ? invalid_type_error : ctx.defaultError + message: (_b = message !== null && message !== void 0 ? message : invalid_type_error) !== null && _b !== void 0 ? _b : ctx.defaultError }; }; return { @@ -977,11 +1013,13 @@ exports.ZodType = ZodType; exports.Schema = ZodType; exports.ZodSchema = ZodType; const cuidRegex = /^c[^\s-]{8,}$/i; -const cuid2Regex = /^[a-z][a-z0-9]*$/; +const cuid2Regex = /^[0-9a-z]+$/; const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/; // const uuidRegex = // /^([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[a-f0-9]{4}-[a-f0-9]{12}|00000000-0000-0000-0000-000000000000)$/i; const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +const nanoidRegex = /^[a-z0-9_-]{21}$/i; +const durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; // from https://stackoverflow.com/a/46181/1550155 // old version: too slow, didn't support unicode // const emailRegex = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; @@ -994,36 +1032,47 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA- // /^[a-zA-Z0-9\.\!\#\$\%\&\'\*\+\/\=\?\^\_\`\{\|\}\~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; // const emailRegex = // /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; -const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; // const emailRegex = // /^[a-z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9\-]+)*$/i; // from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression const _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; let emojiRegex; -const ipv4Regex = /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/; +// faster, simpler, safer +const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; -// Adapted from https://stackoverflow.com/a/3143231 -const datetimeRegex = args => { +// https://stackoverflow.com/questions/7860392/determine-if-string-is-in-base64-using-javascript +const base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +// simple +// const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +// no leap year validation +// const dateRegexSource = `\\d{4}-((0[13578]|10|12)-31|(0[13-9]|1[0-2])-30|(0[1-9]|1[0-2])-(0[1-9]|1\\d|2\\d))`; +// with leap year validation +const dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + // let regex = `\\d{2}:\\d{2}:\\d{2}`; + let regex = `([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d`; if (args.precision) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${args.precision}}Z$`); - } - } else if (args.precision === 0) { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$`); - } - } else { - if (args.offset) { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } else { - return new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`); - } + regex = `${regex}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + regex = `${regex}(\\.\\d+)?`; } -}; + return regex; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +// Adapted from https://stackoverflow.com/a/3143231 +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +exports.datetimeRegex = datetimeRegex; function isValidIP(ip, version) { if ((version === "v4" || !version) && ipv4Regex.test(ip)) { return true; @@ -1045,10 +1094,7 @@ class ZodString extends ZodType { code: ZodError_1.ZodIssueCode.invalid_type, expected: util_1.ZodParsedType.string, received: ctx.parsedType - } - // - ); - + }); return parseUtil_1.INVALID; } const status = new parseUtil_1.ParseStatus(); @@ -1139,6 +1185,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "nanoid", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "cuid") { if (!cuidRegex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1247,6 +1303,38 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + code: ZodError_1.ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "duration", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else if (check.kind === "ip") { if (!isValidIP(input.data, check.version)) { ctx = this._getOrReturnCtx(input, ctx); @@ -1257,6 +1345,16 @@ class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + (0, parseUtil_1.addIssueToContext)(ctx, { + validation: "base64", + code: ZodError_1.ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } } else { util_1.util.assertNever(check); } @@ -1303,6 +1401,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + nanoid(message) { + return this._addCheck({ + kind: "nanoid", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } cuid(message) { return this._addCheck({ kind: "cuid", @@ -1321,6 +1425,12 @@ class ZodString extends ZodType { ...errorUtil_1.errorUtil.errToObj(message) }); } + base64(message) { + return this._addCheck({ + kind: "base64", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } ip(options) { return this._addCheck({ kind: "ip", @@ -1328,12 +1438,13 @@ class ZodString extends ZodType { }); } datetime(options) { - var _a; + var _a, _b; if (typeof options === "string") { return this._addCheck({ kind: "datetime", precision: null, offset: false, + local: false, message: options }); } @@ -1341,9 +1452,36 @@ class ZodString extends ZodType { kind: "datetime", precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, offset: (_a = options === null || options === void 0 ? void 0 : options.offset) !== null && _a !== void 0 ? _a : false, + local: (_b = options === null || options === void 0 ? void 0 : options.local) !== null && _b !== void 0 ? _b : false, + ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) + }); + } + date(message) { + return this._addCheck({ + kind: "date", + message + }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof (options === null || options === void 0 ? void 0 : options.precision) === "undefined" ? null : options === null || options === void 0 ? void 0 : options.precision, ...errorUtil_1.errorUtil.errToObj(options === null || options === void 0 ? void 0 : options.message) }); } + duration(message) { + return this._addCheck({ + kind: "duration", + ...errorUtil_1.errorUtil.errToObj(message) + }); + } regex(regex, message) { return this._addCheck({ kind: "regex", @@ -1428,6 +1566,15 @@ class ZodString extends ZodType { get isDatetime() { return !!this._def.checks.find(ch => ch.kind === "datetime"); } + get isDate() { + return !!this._def.checks.find(ch => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find(ch => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find(ch => ch.kind === "duration"); + } get isEmail() { return !!this._def.checks.find(ch => ch.kind === "email"); } @@ -1440,6 +1587,9 @@ class ZodString extends ZodType { get isUUID() { return !!this._def.checks.find(ch => ch.kind === "uuid"); } + get isNANOID() { + return !!this._def.checks.find(ch => ch.kind === "nanoid"); + } get isCUID() { return !!this._def.checks.find(ch => ch.kind === "cuid"); } @@ -1452,6 +1602,9 @@ class ZodString extends ZodType { get isIP() { return !!this._def.checks.find(ch => ch.kind === "ip"); } + get isBase64() { + return !!this._def.checks.find(ch => ch.kind === "base64"); + } get minLength() { let min = null; for (const ch of this._def.checks) { @@ -2442,9 +2595,10 @@ class ZodObject extends ZodType { const syncPairs = []; for (const pair of pairs) { const key = await pair.key; + const value = await pair.value; syncPairs.push({ key, - value: await pair.value, + value, alwaysSet: pair.alwaysSet }); } @@ -2814,15 +2968,25 @@ const getDiscriminator = type => { return type.options; } else if (type instanceof ZodNativeEnum) { // eslint-disable-next-line ban/ban - return Object.keys(type.enum); + return util_1.util.objectValues(type.enum); } else if (type instanceof ZodDefault) { return getDiscriminator(type._def.innerType); } else if (type instanceof ZodUndefined) { return [undefined]; } else if (type instanceof ZodNull) { return [null]; + } else if (type instanceof ZodOptional) { + return [undefined, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); } else { - return null; + return []; } }; class ZodDiscriminatedUnion extends ZodType { @@ -2886,7 +3050,7 @@ class ZodDiscriminatedUnion extends ZodType { // try { for (const type of options) { const discriminatorValues = getDiscriminator(type.shape[discriminator]); - if (!discriminatorValues) { + if (!discriminatorValues.length) { throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); } for (const value of discriminatorValues) { @@ -3123,7 +3287,8 @@ class ZodRecord extends ZodType { for (const key in ctx.data) { pairs.push({ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), - value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)) + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data }); } if (ctx.common.async) { @@ -3511,6 +3676,10 @@ function createZodEnum(values, params) { }); } class ZodEnum extends ZodType { + constructor() { + super(...arguments); + _ZodEnum_cache.set(this, void 0); + } _parse(input) { if (typeof input.data !== "string") { const ctx = this._getOrReturnCtx(input); @@ -3522,7 +3691,10 @@ class ZodEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (this._def.values.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodEnum_cache, new Set(this._def.values), "f"); + } + if (!__classPrivateFieldGet(this, _ZodEnum_cache, "f").has(input.data)) { const ctx = this._getOrReturnCtx(input); const expectedValues = this._def.values; (0, parseUtil_1.addIssueToContext)(ctx, { @@ -3559,15 +3731,28 @@ class ZodEnum extends ZodType { return enumValues; } extract(values) { - return ZodEnum.create(values); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(values, { + ...this._def, + ...newDef + }); } exclude(values) { - return ZodEnum.create(this.options.filter(opt => !values.includes(opt))); + let newDef = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._def; + return ZodEnum.create(this.options.filter(opt => !values.includes(opt)), { + ...this._def, + ...newDef + }); } } exports.ZodEnum = ZodEnum; +_ZodEnum_cache = new WeakMap(); ZodEnum.create = createZodEnum; class ZodNativeEnum extends ZodType { + constructor() { + super(...arguments); + _ZodNativeEnum_cache.set(this, void 0); + } _parse(input) { const nativeEnumValues = util_1.util.getValidEnumValues(this._def.values); const ctx = this._getOrReturnCtx(input); @@ -3580,7 +3765,10 @@ class ZodNativeEnum extends ZodType { }); return parseUtil_1.INVALID; } - if (nativeEnumValues.indexOf(input.data) === -1) { + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f")) { + __classPrivateFieldSet(this, _ZodNativeEnum_cache, new Set(util_1.util.getValidEnumValues(this._def.values)), "f"); + } + if (!__classPrivateFieldGet(this, _ZodNativeEnum_cache, "f").has(input.data)) { const expectedValues = util_1.util.objectValues(nativeEnumValues); (0, parseUtil_1.addIssueToContext)(ctx, { received: ctx.data, @@ -3596,6 +3784,7 @@ class ZodNativeEnum extends ZodType { } } exports.ZodNativeEnum = ZodNativeEnum; +_ZodNativeEnum_cache = new WeakMap(); ZodNativeEnum.create = (values, params) => { return new ZodNativeEnum({ values: values, @@ -3665,32 +3854,34 @@ class ZodEffects extends ZodType { checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); if (effect.type === "preprocess") { const processed = effect.transform(ctx.data, checkCtx); - if (ctx.common.issues.length) { - return { - status: "dirty", - value: ctx.data - }; - } if (ctx.common.async) { - return Promise.resolve(processed).then(processed => { - return this._def.schema._parseAsync({ + return Promise.resolve(processed).then(async processed => { + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = await this._def.schema._parseAsync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; }); } else { - return this._def.schema._parseSync({ + if (status.value === "aborted") return parseUtil_1.INVALID; + const result = this._def.schema._parseSync({ data: processed, path: ctx.path, parent: ctx }); + if (result.status === "aborted") return parseUtil_1.INVALID; + if (result.status === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + if (status.value === "dirty") return (0, parseUtil_1.DIRTY)(result.value); + return result; } } if (effect.type === "refinement") { - const executeRefinement = (acc - // effect: RefinementEffect - ) => { + const executeRefinement = acc => { const result = effect.refinement(acc, checkCtx); if (ctx.common.async) { return Promise.resolve(result); @@ -4013,10 +4204,16 @@ exports.ZodPipeline = ZodPipeline; class ZodReadonly extends ZodType { _parse(input) { const result = this._def.innerType._parse(input); - if ((0, parseUtil_1.isValid)(result)) { - result.value = Object.freeze(result.value); - } - return result; + const freeze = data => { + if ((0, parseUtil_1.isValid)(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return (0, parseUtil_1.isAsync)(result) ? result.then(data => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; } } exports.ZodReadonly = ZodReadonly; @@ -4027,7 +4224,7 @@ ZodReadonly.create = (type, params) => { ...processCreateParams(params) }); }; -const custom = function (check) { +function custom(check) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; let /** @@ -4059,7 +4256,7 @@ const custom = function (check) { } }); return ZodAny.create(); -}; +} exports.custom = custom; exports.late = { object: ZodObject.lazycreate @@ -4113,7 +4310,7 @@ cls) { let params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { message: `Input not instance of ${cls.name}` }; - return (0, exports.custom)(data => data instanceof cls, params); + return custom(data => data instanceof cls, params); }; exports.instanceof = instanceOfType; const stringType = ZodString.create; @@ -10685,11 +10882,13 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; + const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredential) { + if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value @@ -18190,6 +18389,26 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillInitDataResponse").optional(), + success: _zod.z.object({ + credentials: _zod.z.array(credentialsSchema), + identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), + creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), + serializedInputContext: _zod.z.string() + }).optional(), + error: genericErrorSchema.optional() +}); +const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillCredentialsResponse").optional(), + success: _zod.z.object({ + id: _zod.z.string().optional(), + autogenerated: _zod.z.boolean().optional(), + username: _zod.z.string(), + password: _zod.z.string().optional() + }).optional(), + error: genericErrorSchema.optional() +}); const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18222,6 +18441,11 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ + status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), + credentials: _zod.z.array(credentialsSchema), + availableInputTypes: availableInputTypes1Schema +}); const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({ inputType_credentials: _zod.z.boolean().optional(), inputType_identities: _zod.z.boolean().optional(), @@ -18234,55 +18458,6 @@ const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod third_party_credentials_provider: _zod.z.boolean().optional(), unknown_username_categorization: _zod.z.boolean().optional() }); -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ - generatedPassword: generatedPasswordSchema.optional(), - inputType: _zod.z.string(), - mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), - subType: _zod.z.string(), - trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), - serializedInputContext: _zod.z.string().optional(), - triggerContext: triggerContextSchema.optional() -}); -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillDataResponse").optional(), - success: _zod.z.object({ - credentials: credentialsSchema.optional(), - action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) - }).optional(), - error: genericErrorSchema.optional() -}); -const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ - credentials: outgoingCredentialsSchema.optional(), - trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() -}); -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ - type: _zod.z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypesSchema, - error: genericErrorSchema.optional() -}); -const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillInitDataResponse").optional(), - success: _zod.z.object({ - credentials: _zod.z.array(credentialsSchema), - identities: _zod.z.array(_zod.z.record(_zod.z.unknown())), - creditCards: _zod.z.array(_zod.z.record(_zod.z.unknown())), - serializedInputContext: _zod.z.string() - }).optional(), - error: genericErrorSchema.optional() -}); -const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = _zod.z.object({ - type: _zod.z.literal("getAutofillCredentialsResponse").optional(), - success: _zod.z.object({ - id: _zod.z.string().optional(), - autogenerated: _zod.z.boolean().optional(), - username: _zod.z.string(), - password: _zod.z.string().optional() - }).optional(), - error: genericErrorSchema.optional() -}); -const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ - featureToggles: autofillFeatureTogglesSchema -}); const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = _zod.z.object({ success: _zod.z.boolean().optional(), error: genericErrorSchema.optional() @@ -18317,19 +18492,30 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection }).optional(), error: genericErrorSchema.optional() }); -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ - contentScope: contentScopeSchema, - userUnprotectedDomains: _zod.z.array(_zod.z.string()), - userPreferences: userPreferencesSchema +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = _zod.z.object({ + generatedPassword: generatedPasswordSchema.optional(), + inputType: _zod.z.string(), + mainType: _zod.z.union([_zod.z.literal("credentials"), _zod.z.literal("identities"), _zod.z.literal("creditCards")]), + subType: _zod.z.string(), + trigger: _zod.z.union([_zod.z.literal("userInitiated"), _zod.z.literal("autoprompt"), _zod.z.literal("postSignup")]).optional(), + serializedInputContext: _zod.z.string().optional(), + triggerContext: triggerContextSchema.optional() }); -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ - status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), - credentials: _zod.z.array(credentialsSchema), - availableInputTypes: availableInputTypes1Schema +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = _zod.z.object({ + type: _zod.z.literal("getAutofillDataResponse").optional(), + success: _zod.z.object({ + credentials: credentialsSchema.optional(), + action: _zod.z.union([_zod.z.literal("fill"), _zod.z.literal("focus"), _zod.z.literal("none"), _zod.z.literal("refreshAvailableInputTypes"), _zod.z.literal("acceptGeneratedPassword"), _zod.z.literal("rejectGeneratedPassword")]) + }).optional(), + error: genericErrorSchema.optional() }); -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ - type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), - success: runtimeConfigurationSchema.optional(), +const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ + credentials: outgoingCredentialsSchema.optional(), + trigger: _zod.z.union([_zod.z.literal("partialSave"), _zod.z.literal("formSubmission"), _zod.z.literal("passwordGeneration"), _zod.z.literal("emailProtection")]).optional() +}); +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ + type: _zod.z.literal("getAvailableInputTypesResponse").optional(), + success: availableInputTypesSchema, error: genericErrorSchema.optional() }); const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({ @@ -18342,6 +18528,19 @@ const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProvi success: providerStatusUpdatedSchema, error: genericErrorSchema.optional() }); +const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ + featureToggles: autofillFeatureTogglesSchema +}); +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ + contentScope: contentScopeSchema, + userUnprotectedDomains: _zod.z.array(_zod.z.string()), + userPreferences: userPreferencesSchema +}); +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ + type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), + success: runtimeConfigurationSchema.optional(), + error: genericErrorSchema.optional() +}); const apiSchema = exports.apiSchema = _zod.z.object({ addDebugFlag: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ paramsValidator: addDebugFlagParamsSchema.optional() diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 6063f2bd4..4104fb4b9 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -6519,11 +6519,13 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasOnlyOneCredential = Boolean(formValues.credentials?.username) && !formValues.credentials?.password || Boolean(formValues.credentials?.password) && !formValues.credentials?.username; + const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredential) { + if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value @@ -13894,25 +13896,25 @@ const contentScopeSchema = exports.contentScopeSchema = null; const userPreferencesSchema = exports.userPreferencesSchema = null; const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; const availableInputTypesSchema = exports.availableInputTypesSchema = null; -const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; -const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; -const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; -const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; -const storeFormDataSchema = exports.storeFormDataSchema = null; -const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = null; const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = null; -const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; +const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = null; const emailProtectionGetUserDataResultSchema = exports.emailProtectionGetUserDataResultSchema = null; const emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = null; const emailProtectionGetAddressesResultSchema = exports.emailProtectionGetAddressesResultSchema = null; const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = null; -const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; -const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; +const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; +const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; +const storeFormDataSchema = exports.storeFormDataSchema = null; +const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null; const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null; +const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; +const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; const apiSchema = exports.apiSchema = null; },{}],60:[function(require,module,exports){ From 01dd794f2661d3beea873dd4fcb7ebca7f772e3d Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Tue, 3 Dec 2024 15:57:39 +0100 Subject: [PATCH 07/12] fix: narrow down identities for partial save --- dist/autofill-debug.js | 2 +- dist/autofill.js | 2 +- src/DeviceInterface/InterfacePrototype.js | 3 ++- swift-package/Resources/assets/autofill-debug.js | 2 +- swift-package/Resources/assets/autofill.js | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 4980f6640..c6d9c2616 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9486,7 +9486,7 @@ class InterfacePrototype { // If credentials has only username field, and no password field, then trigger is a partialSave const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } diff --git a/dist/autofill.js b/dist/autofill.js index 4104fb4b9..bf2186d2e 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5123,7 +5123,7 @@ class InterfacePrototype { // If credentials has only username field, and no password field, then trigger is a partialSave const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 9fb5fe1be..3007c8054 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -813,7 +813,8 @@ class InterfacePrototype { // Is an email or phone number present in the form, but no other credentials const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && - [...form.inputs.credentials].length === 0; + [...form.inputs.credentials].length === 0 && + [...form.inputs.identities].length === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 4980f6640..c6d9c2616 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9486,7 +9486,7 @@ class InterfacePrototype { // If credentials has only username field, and no password field, then trigger is a partialSave const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 4104fb4b9..bf2186d2e 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5123,7 +5123,7 @@ class InterfacePrototype { // If credentials has only username field, and no password field, then trigger is a partialSave const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } From 225ca95c98cc16ae25672a677e13eec6cb557138 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Wed, 4 Dec 2024 16:04:45 +0100 Subject: [PATCH 08/12] chore: address PR comments --- dist/autofill-debug.js | 6 +++--- dist/autofill.js | 6 +++--- src/DeviceInterface/InterfacePrototype.js | 6 +++--- src/Form/Form.js | 2 +- src/Form/formatters.test.js | 6 +++--- swift-package/Resources/assets/autofill-debug.js | 6 +++--- swift-package/Resources/assets/autofill.js | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index c6d9c2616..a86fd1aaf 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9484,9 +9484,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } @@ -10886,7 +10886,7 @@ class Form { const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data diff --git a/dist/autofill.js b/dist/autofill.js index bf2186d2e..addd399fa 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5121,9 +5121,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } @@ -6523,7 +6523,7 @@ class Form { const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 3007c8054..3fb39e621 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -809,12 +809,12 @@ class InterfacePrototype { // If credentials has only username field, and no password field, then trigger is a partialSave const isUsernameOnly = - Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; // Is an email or phone number present in the form, but no other credentials const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && - [...form.inputs.credentials].length === 0 && - [...form.inputs.identities].length === 1; + form.inputs.credentials.size === 0 && + form.inputs.identities.size === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } diff --git a/src/Form/Form.js b/src/Form/Form.js index d999b08e2..5f1f07606 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -905,7 +905,7 @@ class Form { const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); - // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data diff --git a/src/Form/formatters.test.js b/src/Form/formatters.test.js index 91203144e..f4c526cae 100644 --- a/src/Form/formatters.test.js +++ b/src/Form/formatters.test.js @@ -61,7 +61,7 @@ describe('Can strip phone formatting characters', () => { describe('prepareFormValuesForStorage()', () => { describe('handling credentials', () => { - it.skip('rejects for username only', () => { + it('accepts for username only', () => { const values = prepareFormValuesForStorage( { credentials: { username: 'dax@example.com' }, @@ -70,9 +70,9 @@ describe('prepareFormValuesForStorage()', () => { // @ts-ignore identities: {}, }, - false, + true, ); - expect(values.credentials).toBeUndefined(); + expect(values.credentials?.username).toBe('dax@example.com'); }); it('accepts password only', () => { const values = prepareFormValuesForStorage( diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index c6d9c2616..a86fd1aaf 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9484,9 +9484,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } @@ -10886,7 +10886,7 @@ class Form { const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index bf2186d2e..addd399fa 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5121,9 +5121,9 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && [...form.inputs.credentials].length === 1; + const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && [...form.inputs.credentials].length === 0 && [...form.inputs.identities].length === 1; + const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } @@ -6523,7 +6523,7 @@ class Form { const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credntial field - then we want to prompt a partial save with username, + // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { // …if we know all the values do not prompt to store data From 15e9ed949e0a427f1bc23a87a3159254753bf526 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Wed, 4 Dec 2024 17:47:50 +0100 Subject: [PATCH 09/12] fix: make credentials storing more liberal --- dist/autofill-debug.js | 34 +++--------- dist/autofill.js | 34 +++--------- src/Form/Form.js | 3 +- src/Form/Form.test.js | 8 +-- src/Form/formatters.js | 20 ++----- src/Form/formatters.test.js | 53 ++++++++----------- .../Resources/assets/autofill-debug.js | 34 +++--------- swift-package/Resources/assets/autofill.js | 34 +++--------- 8 files changed, 62 insertions(+), 158 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index a86fd1aaf..b519752af 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -10268,8 +10268,7 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - const hasOnlyOneCredential = this.inputs.credentials.size === 1; - return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); + return (0, _formatters.prepareFormValuesForStorage)(formValues); } /** @@ -12104,25 +12103,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { - let { - credentials - } = _ref3; - if (credentials.password) { - return Boolean(credentials.password); - } else { - return hasOnlyOneCredential && Boolean(credentials.username); - } -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; @@ -12130,10 +12114,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -12153,11 +12137,10 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues - * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { +const prepareFormValuesForStorage = formValues => { /** @type {Partial} */ let { credentials, @@ -12170,9 +12153,8 @@ const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { + /** Fixes for credentials */ + if (credentials.username || credentials.password) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; diff --git a/dist/autofill.js b/dist/autofill.js index addd399fa..8c3990b33 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5905,8 +5905,7 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - const hasOnlyOneCredential = this.inputs.credentials.size === 1; - return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); + return (0, _formatters.prepareFormValuesForStorage)(formValues); } /** @@ -7741,25 +7740,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { - let { - credentials - } = _ref3; - if (credentials.password) { - return Boolean(credentials.password); - } else { - return hasOnlyOneCredential && Boolean(credentials.username); - } -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; @@ -7767,10 +7751,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -7790,11 +7774,10 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues - * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { +const prepareFormValuesForStorage = formValues => { /** @type {Partial} */ let { credentials, @@ -7807,9 +7790,8 @@ const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { + /** Fixes for credentials */ + if (credentials.username || credentials.password) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; diff --git a/src/Form/Form.js b/src/Form/Form.js index 5f1f07606..4c426cc0c 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -240,8 +240,7 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - const hasOnlyOneCredential = this.inputs.credentials.size === 1; - return prepareFormValuesForStorage(formValues, hasOnlyOneCredential); + return prepareFormValuesForStorage(formValues); } /** diff --git a/src/Form/Form.test.js b/src/Form/Form.test.js index b0890a803..4f6492bee 100644 --- a/src/Form/Form.test.js +++ b/src/Form/Form.test.js @@ -84,8 +84,8 @@ describe('Test the form class reading values correctly', () => { `, - expHasValues: false, - expValues: { credentials: undefined }, + expHasValues: true, + expValues: { credentials: { username: 'testUsername' } }, }, { testCase: 'form where the password is <=3 characters long', @@ -95,8 +95,8 @@ describe('Test the form class reading values correctly', () => { `, - expHasValues: false, - expValues: { credentials: undefined }, + expHasValues: true, + expValues: { credentials: { username: 'testUsername' } }, }, { testCase: 'form with hidden email field', diff --git a/src/Form/formatters.js b/src/Form/formatters.js index 52e0fa3c8..a34d9e383 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -159,18 +159,6 @@ const getMMAndYYYYFromString = (expiration) => { ); }; -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreCredentials = ({ credentials }, hasOnlyOneCredential) => { - if (credentials.password) { - return Boolean(credentials.password); - } else { - return hasOnlyOneCredential && Boolean(credentials.username); - } -}; - /** * @param {InternalDataStorageObject} credentials * @return {boolean} @@ -207,10 +195,9 @@ const formatPhoneNumber = (phone) => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues - * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ -const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { +const prepareFormValuesForStorage = (formValues) => { /** @type {Partial} */ let { credentials, identities, creditCards } = formValues; @@ -219,9 +206,8 @@ const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { + /** Fixes for credentials */ + if (credentials.username || credentials.password) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; diff --git a/src/Form/formatters.test.js b/src/Form/formatters.test.js index f4c526cae..0e12b5333 100644 --- a/src/Form/formatters.test.js +++ b/src/Form/formatters.test.js @@ -62,44 +62,35 @@ describe('Can strip phone formatting characters', () => { describe('prepareFormValuesForStorage()', () => { describe('handling credentials', () => { it('accepts for username only', () => { - const values = prepareFormValuesForStorage( - { - credentials: { username: 'dax@example.com' }, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }, - true, - ); + const values = prepareFormValuesForStorage({ + credentials: { username: 'dax@example.com' }, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }); expect(values.credentials?.username).toBe('dax@example.com'); }); it('accepts password only', () => { - const values = prepareFormValuesForStorage( - { - // @ts-ignore - credentials: { password: '123456' }, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }, - false, - ); + const values = prepareFormValuesForStorage({ + // @ts-ignore + credentials: { password: '123456' }, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }); expect(values.credentials?.password).toBe('123456'); }); it('accepts username+password', () => { const inputCredentials = { username: 'dax@example.com', password: '123456' }; - const values = prepareFormValuesForStorage( - { - credentials: inputCredentials, - // @ts-ignore - creditCards: {}, - // @ts-ignore - identities: {}, - }, - false, - ); + const values = prepareFormValuesForStorage({ + credentials: inputCredentials, + // @ts-ignore + creditCards: {}, + // @ts-ignore + identities: {}, + }); expect(values.credentials).toEqual(inputCredentials); }); }); diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index a86fd1aaf..b519752af 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -10268,8 +10268,7 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - const hasOnlyOneCredential = this.inputs.credentials.size === 1; - return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); + return (0, _formatters.prepareFormValuesForStorage)(formValues); } /** @@ -12104,25 +12103,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { - let { - credentials - } = _ref3; - if (credentials.password) { - return Boolean(credentials.password); - } else { - return hasOnlyOneCredential && Boolean(credentials.username); - } -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; @@ -12130,10 +12114,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -12153,11 +12137,10 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues - * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { +const prepareFormValuesForStorage = formValues => { /** @type {Partial} */ let { credentials, @@ -12170,9 +12153,8 @@ const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { + /** Fixes for credentials */ + if (credentials.username || credentials.password) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index addd399fa..8c3990b33 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5905,8 +5905,7 @@ class Form { */ getValuesReadyForStorage() { const formValues = this.getRawValues(); - const hasOnlyOneCredential = this.inputs.credentials.size === 1; - return (0, _formatters.prepareFormValuesForStorage)(formValues, hasOnlyOneCredential); + return (0, _formatters.prepareFormValuesForStorage)(formValues); } /** @@ -7741,25 +7740,10 @@ const getMMAndYYYYFromString = expiration => { * @return {boolean} */ exports.getMMAndYYYYFromString = getMMAndYYYYFromString; -const shouldStoreCredentials = (_ref3, hasOnlyOneCredential) => { - let { - credentials - } = _ref3; - if (credentials.password) { - return Boolean(credentials.password); - } else { - return hasOnlyOneCredential && Boolean(credentials.username); - } -}; - -/** - * @param {InternalDataStorageObject} credentials - * @return {boolean} - */ -const shouldStoreIdentities = _ref4 => { +const shouldStoreIdentities = _ref3 => { let { identities - } = _ref4; + } = _ref3; return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); }; @@ -7767,10 +7751,10 @@ const shouldStoreIdentities = _ref4 => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreCreditCards = _ref5 => { +const shouldStoreCreditCards = _ref4 => { let { creditCards - } = _ref5; + } = _ref4; if (!creditCards.cardNumber) return false; if (creditCards.cardSecurityCode) return true; // Some forms (Amazon) don't have the cvv, so we still save if there's the expiration @@ -7790,11 +7774,10 @@ const formatPhoneNumber = phone => phone.replaceAll(/[^0-9|+]/g, ''); * Formats form data into an object to send to the device for storage * If values are insufficient for a complete entry, they are discarded * @param {InternalDataStorageObject} formValues - * @param {boolean} hasOnlyOneCredential * @return {DataStorageObject} */ exports.formatPhoneNumber = formatPhoneNumber; -const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { +const prepareFormValuesForStorage = formValues => { /** @type {Partial} */ let { credentials, @@ -7807,9 +7790,8 @@ const prepareFormValuesForStorage = (formValues, hasOnlyOneCredential) => { creditCards.cardName = identities?.fullName || formatFullName(identities); } - /** Fixes for credentials **/ - // Don't store if there isn't enough data - if (shouldStoreCredentials(formValues, hasOnlyOneCredential)) { + /** Fixes for credentials */ + if (credentials.username || credentials.password) { // If we don't have a username to match a password, let's see if the email is available if (credentials.password && !credentials.username && identities.emailAddress) { credentials.username = identities.emailAddress; From 1098e1bab1dd30985817689d45f7227cb38be086 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Thu, 5 Dec 2024 11:46:30 +0100 Subject: [PATCH 10/12] feat: preserve autosubmit --- dist/autofill-debug.js | 12 ++++++++---- dist/autofill.js | 12 ++++++++---- src/Form/Form.js | 13 +++++++++---- swift-package/Resources/assets/autofill-debug.js | 12 ++++++++---- swift-package/Resources/assets/autofill.js | 12 ++++++++---- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 8ed901c95..0b3c6770a 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -10882,13 +10882,17 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, + + // If we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { - // …if we know all the values do not prompt to store data + if (hasOnlyOneCredentialOrEmail) { + this.shouldPromptToStoreData = true; + this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; + } else if (areAllFormValuesKnown) { + // …if it's a normal form with more than one field and if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/dist/autofill.js b/dist/autofill.js index 46f83c612..32824bd96 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -6519,13 +6519,17 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, + + // If we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { - // …if we know all the values do not prompt to store data + if (hasOnlyOneCredentialOrEmail) { + this.shouldPromptToStoreData = true; + this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; + } else if (areAllFormValuesKnown) { + // …if it's a normal form with more than one field and if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/src/Form/Form.js b/src/Form/Form.js index 4c426cc0c..49ecc5b9e 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -896,7 +896,8 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyEmail = + formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || @@ -904,10 +905,14 @@ class Form { const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); - // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, + + // If we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { - // …if we know all the values do not prompt to store data + if (hasOnlyOneCredentialOrEmail) { + this.shouldPromptToStoreData = true; + this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; + } else if (areAllFormValuesKnown) { + // …if it's a normal form with more than one field and if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 8ed901c95..0b3c6770a 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -10882,13 +10882,17 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, + + // If we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { - // …if we know all the values do not prompt to store data + if (hasOnlyOneCredentialOrEmail) { + this.shouldPromptToStoreData = true; + this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; + } else if (areAllFormValuesKnown) { + // …if it's a normal form with more than one field and if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 46f83c612..32824bd96 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -6519,13 +6519,17 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities).length === 1 && formValues.identities.emailAddress; + const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - // If all form values are known, but we only have a single credential field - then we want to prompt a partial save with username, + + // If we only have a single credential field - then we want to prompt a partial save with username, // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (areAllFormValuesKnown && !hasOnlyOneCredentialOrEmail) { - // …if we know all the values do not prompt to store data + if (hasOnlyOneCredentialOrEmail) { + this.shouldPromptToStoreData = true; + this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; + } else if (areAllFormValuesKnown) { + // …if it's a normal form with more than one field and if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; From c6b596f48c54d83390ab3e576f116dc01091af3e Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Fri, 6 Dec 2024 11:45:43 +0100 Subject: [PATCH 11/12] refactor: simply value storage --- dist/autofill-debug.js | 52 ++++++++++--------- dist/autofill.js | 52 ++++++++++--------- src/DeviceInterface/InterfacePrototype.js | 12 ++--- src/Form/Form.js | 17 +----- src/Form/Form.test.js | 6 +-- src/Form/formatters.js | 23 ++++---- src/autofill-utils.js | 10 ++++ .../schemas/storeFormData.params.json | 2 +- .../Resources/assets/autofill-debug.js | 52 ++++++++++--------- swift-package/Resources/assets/autofill.js | 52 ++++++++++--------- 10 files changed, 135 insertions(+), 143 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 0b3c6770a..5a04ddd63 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9476,7 +9476,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -9484,10 +9485,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10881,18 +10879,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -11936,6 +11925,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -12112,7 +12102,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -12159,12 +12149,13 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -12220,7 +12211,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17157,7 +17148,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -17825,6 +17818,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * Checks if there's only one identity in the object, and it's a username-like identity + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; diff --git a/dist/autofill.js b/dist/autofill.js index 32824bd96..07aa7fb35 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5113,7 +5113,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -5121,10 +5122,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -6518,18 +6516,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -7573,6 +7562,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -7749,7 +7739,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -7796,12 +7786,13 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -7857,7 +7848,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12794,7 +12785,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -13462,6 +13455,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * Checks if there's only one identity in the object, and it's a username-like identity + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict"; diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 3fb39e621..b81022bb8 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -799,8 +799,9 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = appendGeneratedKey(values, { password: this.passwordGenerator.password, @@ -808,14 +809,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = - Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = - Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && - form.inputs.credentials.size === 0 && - form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } diff --git a/src/Form/Form.js b/src/Form/Form.js index 49ecc5b9e..e5fcb46ad 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -895,24 +895,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = - formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - - const hasOnlyOneCredentialOrEmail = - Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || - (hasOnlyEmail && hasNoCredentialsData); const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/src/Form/Form.test.js b/src/Form/Form.test.js index 4f6492bee..bf69ff3e9 100644 --- a/src/Form/Form.test.js +++ b/src/Form/Form.test.js @@ -276,11 +276,7 @@ describe('Test the form class reading values correctly', () => { `, expHasValues: true, expValues: { - identities: { - emailAddress: 'peppapig@email.com', - firstName: 'Peppa', - lastName: 'Pig', - }, + identities: undefined, creditCards: { cardName: 'Peppa Pig', cardSecurityCode: '123', diff --git a/src/Form/formatters.js b/src/Form/formatters.js index a34d9e383..7a34a80e9 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -1,5 +1,6 @@ import { matchInPlaceholderAndLabels, checkPlaceholderAndLabels } from './matching.js'; import { COUNTRY_CODES_TO_NAMES, COUNTRY_NAMES_TO_CODES } from './countryNames.js'; +import { hasUsernameLikeIdentity } from '../autofill-utils.js'; // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; @@ -163,13 +164,8 @@ const getMMAndYYYYFromString = (expiration) => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreIdentities = ({ identities }) => { - return ( - Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || - Boolean(identities.emailAddress) || - Boolean(identities.phone) - ); -}; +const shouldStoreIdentities = ({ identities }) => + Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); /** * @param {InternalDataStorageObject} credentials @@ -207,12 +203,13 @@ const prepareFormValuesForStorage = (formValues) => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && hasUsernameLikeIdentity(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 96eba5b48..325510bda 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -624,6 +624,15 @@ function queryElementsWithShadow(element, selector, forceScanShadowTree = false) return [...elements]; } +/** + * Checks if there's only one identity in the object, and it's a username-like identity + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + export { notifyWebApp, sendAndWaitForAnswer, @@ -659,4 +668,5 @@ export { findElementsInShadowTree, queryElementsWithShadow, getFormControlElements, + hasUsernameLikeIdentity, }; diff --git a/src/deviceApiCalls/schemas/storeFormData.params.json b/src/deviceApiCalls/schemas/storeFormData.params.json index 8b86c5573..9be93231c 100644 --- a/src/deviceApiCalls/schemas/storeFormData.params.json +++ b/src/deviceApiCalls/schemas/storeFormData.params.json @@ -30,4 +30,4 @@ ] } } -} \ No newline at end of file +} diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 0b3c6770a..5a04ddd63 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9476,7 +9476,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -9484,10 +9485,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10881,18 +10879,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -11936,6 +11925,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -12112,7 +12102,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -12159,12 +12149,13 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -12220,7 +12211,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17157,7 +17148,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -17825,6 +17818,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * Checks if there's only one identity in the object, and it's a username-like identity + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 32824bd96..07aa7fb35 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5113,7 +5113,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -5121,10 +5122,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -6518,18 +6516,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store data this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -7573,6 +7562,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -7749,7 +7739,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -7796,12 +7786,13 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + // If we still don't have any credentials, we discard the object + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -7857,7 +7848,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12794,7 +12785,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -13462,6 +13455,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * Checks if there's only one identity in the object, and it's a username-like identity + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict"; From 8f1e4762ee82f7bcd1ddd53256a888c9048b25f7 Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Fri, 6 Dec 2024 14:08:24 +0100 Subject: [PATCH 12/12] style: minor PR comments --- dist/autofill-debug.js | 6 +++--- dist/autofill.js | 6 +++--- src/DeviceInterface/InterfacePrototype.js | 2 +- src/Form/formatters.js | 2 +- src/autofill-utils.js | 2 +- swift-package/Resources/assets/autofill-debug.js | 6 +++--- swift-package/Resources/assets/autofill.js | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 5a04ddd63..91c893b10 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9476,7 +9476,7 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { @@ -12151,7 +12151,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials */ if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { // @ts-ignore - We know that username is not a useful value here - credentials.username = identities.emailAddress ?? identities.phone; + credentials.username = identities.emailAddress || identities.phone; } // If we still don't have any credentials, we discard the object @@ -17819,7 +17819,7 @@ function queryElementsWithShadow(element, selector) { } /** - * Checks if there's only one identity in the object, and it's a username-like identity + * Checks if there is a single username-like identity, i.e. email or phone * @param {InternalIdentityObject} identities * @returns {boolean} */ diff --git a/dist/autofill.js b/dist/autofill.js index 07aa7fb35..f9de41412 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5113,7 +5113,7 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { @@ -7788,7 +7788,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials */ if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { // @ts-ignore - We know that username is not a useful value here - credentials.username = identities.emailAddress ?? identities.phone; + credentials.username = identities.emailAddress || identities.phone; } // If we still don't have any credentials, we discard the object @@ -13456,7 +13456,7 @@ function queryElementsWithShadow(element, selector) { } /** - * Checks if there's only one identity in the object, and it's a username-like identity + * Checks if there is a single username-like identity, i.e. email or phone * @param {InternalIdentityObject} identities * @returns {boolean} */ diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index b81022bb8..51fb4398f 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -800,7 +800,7 @@ class InterfacePrototype { if (!form.form) return; if (!form.hasValues(values)) return; - const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = appendGeneratedKey(values, { diff --git a/src/Form/formatters.js b/src/Form/formatters.js index 7a34a80e9..976145926 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -205,7 +205,7 @@ const prepareFormValuesForStorage = (formValues) => { /** Fixes for credentials */ if (!credentials.username && hasUsernameLikeIdentity(identities)) { // @ts-ignore - We know that username is not a useful value here - credentials.username = identities.emailAddress ?? identities.phone; + credentials.username = identities.emailAddress || identities.phone; } // If we still don't have any credentials, we discard the object diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 325510bda..99a11fdc4 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -625,7 +625,7 @@ function queryElementsWithShadow(element, selector, forceScanShadowTree = false) } /** - * Checks if there's only one identity in the object, and it's a username-like identity + * Checks if there is a single username-like identity, i.e. email or phone * @param {InternalIdentityObject} identities * @returns {boolean} */ diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 5a04ddd63..91c893b10 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9476,7 +9476,7 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { @@ -12151,7 +12151,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials */ if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { // @ts-ignore - We know that username is not a useful value here - credentials.username = identities.emailAddress ?? identities.phone; + credentials.username = identities.emailAddress || identities.phone; } // If we still don't have any credentials, we discard the object @@ -17819,7 +17819,7 @@ function queryElementsWithShadow(element, selector) { } /** - * Checks if there's only one identity in the object, and it's a username-like identity + * Checks if there is a single username-like identity, i.e. email or phone * @param {InternalIdentityObject} identities * @returns {boolean} */ diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 07aa7fb35..f9de41412 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5113,7 +5113,7 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const isUsernameOnly = Object.keys(values?.credentials || {}).length === 1 && values?.credentials?.username; const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { @@ -7788,7 +7788,7 @@ const prepareFormValuesForStorage = formValues => { /** Fixes for credentials */ if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { // @ts-ignore - We know that username is not a useful value here - credentials.username = identities.emailAddress ?? identities.phone; + credentials.username = identities.emailAddress || identities.phone; } // If we still don't have any credentials, we discard the object @@ -13456,7 +13456,7 @@ function queryElementsWithShadow(element, selector) { } /** - * Checks if there's only one identity in the object, and it's a username-like identity + * Checks if there is a single username-like identity, i.e. email or phone * @param {InternalIdentityObject} identities * @returns {boolean} */