Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Form] Trigger partialSave on username/email only form submit #702

Merged
merged 17 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions dist/autofill-debug.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 25 additions & 22 deletions dist/autofill.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions integration-test/helpers/pages/loginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions integration-test/tests/save-prompts.android.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions integration-test/tests/save-prompts.ios.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down
4 changes: 2 additions & 2 deletions integration-test/tests/save-prompts.macos.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();
});
});
});
Expand Down
12 changes: 11 additions & 1 deletion src/DeviceInterface/InterfacePrototype.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,17 @@ 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 isUsernameOnly =
Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you just check for username and size here? Also, don't need to force to Boolean.

Suggested change
Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1;
formData.credentials?.username && 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See below, we may be able to get rid of these identities checks.

const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission';
this.storeFormData(formData, trigger);
}
}

Expand Down
17 changes: 15 additions & 2 deletions src/Form/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -895,11 +895,24 @@ 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complexity of this stuff blows my mind 🙃. Can you please take another look and make it simpler? Like, maybe you can first check whether the form has either an email or username, and then check that there is nothing else? Or even better return early if there is more than 1 item, then check if it's a username or email.

Also maybe move it to a utility function? Not sure how complex it is after you simplify it. Feel free to ping me on MM with a preview.

const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(
(subtype) => formValues[dataType][subtype] === data[subtype],
);
if (areAllFormValuesKnown) {
// …if we know all the values do not prompt to store data

// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to autosubmit here, do we? Imagine people just generating a private duck address on a newsletter form. We don't want to automatically submit that. Am I missing anything?

} 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;
Expand Down
14 changes: 9 additions & 5 deletions src/Form/Form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ describe('Test the form class reading values correctly', () => {
<input type="password" value="" autocomplete="new-password" />
<button type="submit">Sign up</button>
</form>`,
expHasValues: false,
expValues: { credentials: undefined },
expHasValues: true,
expValues: { credentials: { username: 'testUsername' } },
},
{
testCase: 'form where the password is <=3 characters long',
Expand All @@ -95,8 +95,8 @@ describe('Test the form class reading values correctly', () => {
<input type="password" value="abc" autocomplete="new-password" />
<button type="submit">Sign up</button>
</form>`,
expHasValues: false,
expValues: { credentials: undefined },
expHasValues: true,
expValues: { credentials: { username: 'testUsername' } },
},
{
testCase: 'form with hidden email field',
Expand Down Expand Up @@ -276,7 +276,11 @@ describe('Test the form class reading values correctly', () => {
</form>`,
expHasValues: true,
expValues: {
identities: undefined,
identities: {
emailAddress: '[email protected]',
firstName: 'Peppa',
lastName: 'Pig',
},
creditCards: {
cardName: 'Peppa Pig',
cardSecurityCode: '123',
Expand Down
20 changes: 9 additions & 11 deletions src/Form/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,13 @@ const getMMAndYYYYFromString = (expiration) => {
* @param {InternalDataStorageObject} credentials
* @return {boolean}
*/
const shouldStoreCredentials = ({ credentials }) => Boolean(credentials.password);

/**
* @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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this start prompting to save identities that just have a lone phone or lone email? Do we have some backend checks in the native apps to prevent that?

Copy link
Member

@GioSensation GioSensation Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, we have a guard against it in the interface prototype. Got it.

But that makes this whole thing confusing because we're saying that we should store identities when in fact we shouldn't.

What I think we should do instead is: when we detect one of these email-only or phone-only forms, move that email or phone to credentials.username, then let the rest of the flow continue as usual. Because that's actually what we want, logically. It's not an identity, it's a username. Makes sense? I think this should also simplify things on the native side, because they can only expect these updated calls to have credentials.username rather than multiple potential types.

);
};

/**
* @param {InternalDataStorageObject} credentials
Expand Down Expand Up @@ -207,9 +206,8 @@ const prepareFormValuesForStorage = (formValues) => {
creditCards.cardName = identities?.fullName || formatFullName(identities);
}

/** Fixes for credentials **/
// Don't store if there isn't enough data
if (shouldStoreCredentials(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;
Expand Down
4 changes: 2 additions & 2 deletions src/Form/formatters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ describe('Can strip phone formatting characters', () => {

describe('prepareFormValuesForStorage()', () => {
describe('handling credentials', () => {
it('rejects for username only', () => {
it('accepts for username only', () => {
const values = prepareFormValuesForStorage({
credentials: { username: '[email protected]' },
// @ts-ignore
creditCards: {},
// @ts-ignore
identities: {},
});
expect(values.credentials).toBeUndefined();
expect(values.credentials?.username).toBe('[email protected]');
});
it('accepts password only', () => {
const values = prepareFormValuesForStorage({
Expand Down
2 changes: 1 addition & 1 deletion src/device-interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ interface DataStorageObject {
credentials?: CredentialsObject;
creditCards?: CreditCardObject;
identities?: IdentityObject;
trigger?: 'formSubmission' | 'passwordGeneration' | 'emailProtection';
trigger?: 'partialSave' | 'formSubmission' | 'passwordGeneration' | 'emailProtection';
}

interface InternalDataStorageObject {
Expand Down
2 changes: 1 addition & 1 deletion src/deviceApiCalls/__generated__/validators-ts.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading