Bug 1703976 - [1.0] Extend Autocomplete API to support credit card saving r=geckoview-reviewers,agi

Differential Revision: https://phabricator.services.mozilla.com/D111341
This commit is contained in:
owlishDeveloper 2021-09-27 20:16:25 +00:00
Родитель 56ed4f6cd9
Коммит de738da4c8
10 изменённых файлов: 484 добавлений и 9 удалений

Просмотреть файл

@ -39,7 +39,7 @@ class LoginStorageDelegate {
hint |= LoginStorageHint.GENERATED;
}
return {
// Sync with GeckoSession.handlePromptEvent.
// Sync with PromptController
type: "Autocomplete:Save:Login",
hint,
logins: aLogins,

Просмотреть файл

@ -23,6 +23,7 @@ import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
import org.mozilla.geckoview.Autocomplete.Address
import org.mozilla.geckoview.Autocomplete.AddressSelectOption
import org.mozilla.geckoview.Autocomplete.CreditCard
import org.mozilla.geckoview.Autocomplete.CreditCardSaveOption
import org.mozilla.geckoview.Autocomplete.CreditCardSelectOption
import org.mozilla.geckoview.Autocomplete.LoginEntry
import org.mozilla.geckoview.Autocomplete.LoginSaveOption
@ -103,7 +104,7 @@ class AutocompleteTest : BaseSessionTest() {
val name = arrayOf("Peter Parker", "John Doe")
val number = arrayOf("1234-1234-1234-1234", "2345-2345-2345-2345")
val guid = arrayOf("test-guid1", "test-guid2")
val expMonth = arrayOf("Apr", "Aug")
val expMonth = arrayOf("04", "08")
val expYear = arrayOf("22", "23")
val savedCC = arrayOf(
CreditCard.Builder()
@ -132,6 +133,9 @@ class AutocompleteTest : BaseSessionTest() {
: GeckoResult<Array<CreditCard>>? {
return GeckoResult.fromValue(savedCC)
}
@AssertCalled(false)
override fun onCreditCardSave(creditCard: CreditCard) {}
})
mainSession.delegateUntilTestEnd(object : PromptDelegate {
@ -341,7 +345,6 @@ class AutocompleteTest : BaseSessionTest() {
checkAddressesForCorrectness(savedAddresses.toTypedArray(), savedAddress)
}
@Test
fun addressSelectAndFillMultipleAddresses() {
val givenNames = arrayOf("Peter", "Wade")
@ -690,6 +693,340 @@ class AutocompleteTest : BaseSessionTest() {
sessionRule.waitForResult(saveHandled2)
}
@Test
fun creditCardSaveAccept() {
val ccName = "MyCard"
val ccNumber = "5105-1051-0510-5100"
val ccExpMonth = "06"
val ccExpYear = "24"
mainSession.loadTestPath(CC_FORM_HTML_PATH)
mainSession.waitForPageStop()
val saveHandled = GeckoResult<Void>()
sessionRule.delegateUntilTestEnd(object : StorageDelegate {
@AssertCalled
override fun onCreditCardSave(creditCard: CreditCard) {
assertThat("Credit card name should match", creditCard.name, equalTo(ccName))
assertThat("Credit card number should match", creditCard.number, equalTo(ccNumber))
assertThat("Credit card expiration month should match", creditCard.expirationMonth, equalTo(ccExpMonth))
assertThat("Credit card expiration year should match", creditCard.expirationYear, equalTo(ccExpYear))
saveHandled.complete(null)
}
})
sessionRule.delegateDuringNextWait(object : PromptDelegate {
@AssertCalled
override fun onCreditCardSave(
session: GeckoSession,
request: AutocompleteRequest<CreditCardSaveOption>)
: GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Session should not be null", session, notNullValue())
val option = request.options[0]
val cc = option.value
assertThat("Credit card should not be null", cc, notNullValue())
assertThat(
"Credit card name should match",
cc.name,
equalTo(ccName))
assertThat(
"Credit card number should match",
cc.number,
equalTo(ccNumber))
assertThat(
"Credit card expiration month should match",
cc.expirationMonth,
equalTo(ccExpMonth))
assertThat(
"Credit card expiration year should match",
cc.expirationYear,
equalTo(ccExpYear))
return GeckoResult.fromValue(request.confirm(option))
}
})
// Enter the card values
mainSession.evaluateJS("document.querySelector('#name').focus()")
mainSession.evaluateJS("document.querySelector('#name').value = '${ccName}'")
mainSession.evaluateJS("document.querySelector('#number').value = '${ccNumber}'")
mainSession.evaluateJS("document.querySelector('#expMonth').value = '${ccExpMonth}'")
mainSession.evaluateJS("document.querySelector('#expYear').value = '${ccExpYear}'")
// Submit the form
mainSession.evaluateJS("document.querySelector('form').submit()")
sessionRule.waitForResult(saveHandled)
}
@Test
fun creditCardSaveDismiss() {
val ccName = "MyCard"
val ccNumber = "5105-1051-0510-5100"
val ccExpMonth = "06"
val ccExpYear = "24"
mainSession.loadTestPath(CC_FORM_HTML_PATH)
mainSession.waitForPageStop()
sessionRule.delegateDuringNextWait(object : StorageDelegate {
@AssertCalled
override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? {
return null
}
})
sessionRule.delegateUntilTestEnd(object : StorageDelegate {
@AssertCalled(count = 0)
override fun onCreditCardSave(creditCard: CreditCard) {}
})
// Enter the card values
mainSession.evaluateJS("document.querySelector('#name').focus()")
mainSession.evaluateJS("document.querySelector('#name').value = '${ccName}'")
mainSession.evaluateJS("document.querySelector('#number').value = '${ccNumber}'")
mainSession.evaluateJS("document.querySelector('#expMonth').value = '${ccExpMonth}'")
mainSession.evaluateJS("document.querySelector('#expYear').value = '${ccExpYear}'")
// Submit the form
mainSession.evaluateJS("document.querySelector('form').submit()")
sessionRule.waitUntilCalled(object : PromptDelegate {
@AssertCalled
override fun onCreditCardSave(
session: GeckoSession,
request: AutocompleteRequest<CreditCardSaveOption>)
: GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Session should not be null", session, notNullValue())
val option = request.options[0]
val cc = option.value
assertThat("Credit card should not be null", cc, notNullValue())
assertThat(
"Credit card name should match",
cc.name,
equalTo(ccName))
assertThat(
"Credit card number should match",
cc.number,
equalTo(ccNumber))
assertThat(
"Credit card expiration month should match",
cc.expirationMonth,
equalTo(ccExpMonth))
assertThat(
"Credit card expiration year should match",
cc.expirationYear,
equalTo(ccExpYear))
return GeckoResult.fromValue(request.dismiss())
}
})
}
@Test
fun creditCardSaveModifyAccept() {
val ccName = "MyCard"
val ccNumber = "5105-1051-0510-5100"
val ccExpMonth = "06"
val ccExpYearNew = "26"
val ccExpYear = "24"
mainSession.loadTestPath(CC_FORM_HTML_PATH)
mainSession.waitForPageStop()
val saveHandled = GeckoResult<Void>()
sessionRule.delegateUntilTestEnd(object : StorageDelegate {
@AssertCalled
override fun onCreditCardSave(creditCard: CreditCard) {
assertThat("Credit card name should match", creditCard.name, equalTo(ccName))
assertThat("Credit card number should match", creditCard.number, equalTo(ccNumber))
assertThat("Credit card expiration month should match", creditCard.expirationMonth, equalTo(ccExpMonth))
assertThat("Credit card expiration year should match", creditCard.expirationYear, equalTo(ccExpYearNew))
saveHandled.complete(null)
}
})
sessionRule.delegateDuringNextWait(object : PromptDelegate {
@AssertCalled
override fun onCreditCardSave(
session: GeckoSession,
request: AutocompleteRequest<CreditCardSaveOption>)
: GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Session should not be null", session, notNullValue())
val option = request.options[0]
val cc = option.value
assertThat("Credit card should not be null", cc, notNullValue())
assertThat(
"Credit card name should match",
cc.name,
equalTo(ccName))
assertThat(
"Credit card number should match",
cc.number,
equalTo(ccNumber))
assertThat(
"Credit card expiration month should match",
cc.expirationMonth,
equalTo(ccExpMonth))
assertThat(
"Credit card expiration year should match",
cc.expirationYear,
equalTo(ccExpYear))
val modifiedCreditCard = CreditCard.Builder()
.name(cc.name)
.number(cc.number)
.expirationMonth(cc.expirationMonth)
.expirationYear(ccExpYearNew)
.build()
return GeckoResult.fromValue(request.confirm(CreditCardSaveOption(modifiedCreditCard)))
}
})
// Enter the card values
mainSession.evaluateJS("document.querySelector('#name').focus()")
mainSession.evaluateJS("document.querySelector('#name').value = '${ccName}'")
mainSession.evaluateJS("document.querySelector('#number').value = '${ccNumber}'")
mainSession.evaluateJS("document.querySelector('#expMonth').value = '${ccExpMonth}'")
mainSession.evaluateJS("document.querySelector('#expYear').value = '${ccExpYear}'")
// Submit the form
mainSession.evaluateJS("document.querySelector('form').submit()")
sessionRule.waitForResult(saveHandled)
}
@Test
fun creditCardUpdateAccept() {
val ccName = "MyCard"
val ccNumber1 = "5105-1051-0510-5100"
val ccExpMonth1 = "06"
val ccExpYear1 = "24"
val ccNumber2 = "4111-1111-1111-1111"
val ccExpMonth2 = "11"
val ccExpYear2 = "21"
val savedCreditCards = mutableListOf<CreditCard>()
mainSession.loadTestPath(CC_FORM_HTML_PATH)
mainSession.waitForPageStop()
val saveHandled1 = GeckoResult<Void>()
val saveHandled2 = GeckoResult<Void>()
sessionRule.delegateUntilTestEnd(object : StorageDelegate {
@AssertCalled
override fun onCreditCardFetch(): GeckoResult<Array<CreditCard>>? {
return GeckoResult.fromValue(savedCreditCards.toTypedArray())
}
@AssertCalled(count = 2)
override fun onCreditCardSave(creditCard: CreditCard) {
assertThat(
"Credit card name should match",
creditCard.name,
equalTo(ccName))
assertThat(
"Credit card number should match",
creditCard.number,
equalTo(forEachCall(ccNumber1, ccNumber2)))
assertThat(
"Credit card expiration month should match",
creditCard.expirationMonth,
equalTo(forEachCall(ccExpMonth1, ccExpMonth2)))
assertThat(
"Credit card expiration year should match",
creditCard.expirationYear,
equalTo(forEachCall(ccExpYear1, ccExpYear2)))
val savedCC = CreditCard.Builder()
.guid("test1")
.name(creditCard.name)
.number(creditCard.number)
.expirationMonth(creditCard.expirationMonth)
.expirationYear(creditCard.expirationYear)
.build()
savedCreditCards.add(savedCC)
if (sessionRule.currentCall.counter == 1) {
saveHandled1.complete(null)
} else if (sessionRule.currentCall.counter == 2) {
saveHandled2.complete(null)
}
}
})
sessionRule.delegateUntilTestEnd(object : PromptDelegate {
@AssertCalled(count = 2)
override fun onCreditCardSave(
session: GeckoSession,
request: AutocompleteRequest<CreditCardSaveOption>)
: GeckoResult<PromptDelegate.PromptResponse>? {
assertThat("Session should not be null", session, notNullValue())
val option = request.options[0]
val cc = option.value
assertThat("Credit card should not be null", cc, notNullValue())
assertThat(
"Credit card name should match",
cc.name,
equalTo(ccName))
assertThat(
"Credit card number should match",
cc.number,
equalTo(forEachCall(ccNumber1, ccNumber2)))
assertThat(
"Credit card expiration month should match",
cc.expirationMonth,
equalTo(forEachCall(ccExpMonth1, ccExpMonth2)))
assertThat(
"Credit card expiration year should match",
cc.expirationYear,
equalTo(forEachCall(ccExpYear1, ccExpYear2)))
return GeckoResult.fromValue(request.confirm(option))
}
})
// Enter the card values
mainSession.evaluateJS("document.querySelector('#name').focus()")
mainSession.evaluateJS("document.querySelector('#name').value = '${ccName}'")
mainSession.evaluateJS("document.querySelector('#number').value = '${ccNumber1}'")
mainSession.evaluateJS("document.querySelector('#expMonth').value = '${ccExpMonth1}'")
mainSession.evaluateJS("document.querySelector('#expYear').value = '${ccExpYear1}'")
// Submit the form
mainSession.evaluateJS("document.querySelector('form').submit()")
sessionRule.waitForResult(saveHandled1)
// Update credit card
val session2 = sessionRule.createOpenSession()
session2.loadTestPath(CC_FORM_HTML_PATH)
session2.waitForPageStop()
session2.evaluateJS("document.querySelector('#name').focus()")
session2.evaluateJS("document.querySelector('#name').value = '${ccName}'")
session2.evaluateJS("document.querySelector('#number').value = '${ccNumber2}'")
session2.evaluateJS("document.querySelector('#expMonth').value = '${ccExpMonth2}'")
session2.evaluateJS("document.querySelector('#expYear').value = '${ccExpYear2}'")
session2.evaluateJS("document.querySelector('form').submit()")
sessionRule.waitForResult(saveHandled2)
}
fun testLoginUsed(autofillEnabled: Boolean) {
sessionRule.setPrefsUntilTestEnd(mapOf(
// Enable login management since it's disabled in automation.

Просмотреть файл

@ -210,8 +210,12 @@ public class Autocomplete {
bundle.putString(GUID_KEY, guid);
bundle.putString(NAME_KEY, name);
bundle.putString(NUMBER_KEY, number);
bundle.putString(EXP_MONTH_KEY, expirationMonth);
bundle.putString(EXP_YEAR_KEY, expirationYear);
if (expirationMonth != null) {
bundle.putString(EXP_MONTH_KEY, expirationMonth);
}
if (expirationYear != null) {
bundle.putString(EXP_YEAR_KEY, expirationYear);
}
return bundle;
}
@ -949,6 +953,17 @@ public class Autocomplete {
@UiThread
default void onLoginSave(@NonNull final LoginEntry login) {}
/**
* Request saving or updating of the given credit card entry.
* This is triggered by confirming a
* {@link GeckoSession.PromptDelegate#onCreditCardSave onCreditCardSave} request.
*
* @param creditCard The {@link CreditCard} as confirmed by the prompt
* request.
*/
@UiThread
default void onCreditCardSave(@NonNull CreditCard creditCard) {}
/**
* Request saving or updating of the given address entry.
* This is triggered by confirming a
@ -1174,6 +1189,40 @@ public class Autocomplete {
}
}
/**
* Holds information required to process credit card saving requests.
*/
public static class CreditCardSaveOption extends SaveOption<CreditCard> {
/**
* Construct a credit card save option.
*
* @param value The {@link CreditCard} credit card entry to be saved.
* @param hint The {@link Hint} detailing the type of the option.
*/
/* package */ CreditCardSaveOption(
final @NonNull CreditCard value,
final @SaveOptionHint int hint) {
super(value, hint);
}
/**
* Construct a credit card save option.
*
* @param value The {@link CreditCard} credit card entry to be saved.
*/
public CreditCardSaveOption(final @NonNull CreditCard value) {
this(value, Hint.NONE);
}
@Override
/* package */ @NonNull GeckoBundle toBundle() {
final GeckoBundle bundle = new GeckoBundle(2);
bundle.putBundle(VALUE_KEY, value.toBundle());
bundle.putInt(HINT_KEY, hint);
return bundle;
}
}
/**
* Holds information required to process login selection requests.
*/
@ -1349,6 +1398,8 @@ public class Autocomplete {
"GeckoView:Autocomplete:Fetch:Address";
private static final String SAVE_LOGIN_EVENT =
"GeckoView:Autocomplete:Save:Login";
private static final String SAVE_CREDIT_CARD_EVENT =
"GeckoView:Autocomplete:Save:CreditCard";
private static final String SAVE_ADDRESS_EVENT =
"GeckoView:Autocomplete:Save:Address";
private static final String USED_LOGIN_EVENT =
@ -1365,6 +1416,7 @@ public class Autocomplete {
FETCH_CREDIT_CARD_EVENT,
FETCH_ADDRESS_EVENT,
SAVE_LOGIN_EVENT,
SAVE_CREDIT_CARD_EVENT,
SAVE_ADDRESS_EVENT,
USED_LOGIN_EVENT);
}
@ -1376,6 +1428,7 @@ public class Autocomplete {
FETCH_CREDIT_CARD_EVENT,
FETCH_ADDRESS_EVENT,
SAVE_LOGIN_EVENT,
SAVE_CREDIT_CARD_EVENT,
SAVE_ADDRESS_EVENT,
USED_LOGIN_EVENT);
}
@ -1491,6 +1544,11 @@ public class Autocomplete {
final LoginEntry login = new LoginEntry(loginBundle);
mDelegate.onLoginSave(login);
} else if (SAVE_CREDIT_CARD_EVENT.equals(event)) {
final GeckoBundle creditCardBundle = message.getBundle("creditCard");
final CreditCard creditCard = new CreditCard(creditCardBundle);
mDelegate.onCreditCardSave(creditCard);
} else if (SAVE_ADDRESS_EVENT.equals(event)) {
final GeckoBundle addressBundle = message.getBundle("address");
final Address address = new Address(addressBundle);

Просмотреть файл

@ -4994,6 +4994,35 @@ public class GeckoSession {
return null;
}
/**
* Handle a credit card save prompt request.
* This is triggered by the user entering new or modified credit card
* credentials into a form.
*
* @param session The {@link GeckoSession} that triggered the request.
* @param request The {@link AutocompleteRequest} containing the request
* details.
*
* @return A {@link GeckoResult} resolving to a {@link PromptResponse}.
*
* Confirm the request with an {@link Autocomplete.Option}
* to trigger a
* {@link Autocomplete.StorageDelegate#onCreditCardSave} request
* to save the given selection.
* The confirmed selection may be an entry out of the request's
* options, a modified option, or a freshly created credit
* card entry.
*
* Dismiss the request to deny the saving request.
*/
@UiThread
default @Nullable GeckoResult<PromptResponse> onCreditCardSave(
@NonNull final GeckoSession session,
@NonNull final AutocompleteRequest<Autocomplete.CreditCardSaveOption>
request) {
return null;
}
/**
* Handle a login selection prompt request.
* This is triggered by the user focusing on a login username field.

Просмотреть файл

@ -9,6 +9,7 @@ import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.geckoview.Autocomplete.AddressSaveOption;
import org.mozilla.geckoview.Autocomplete.AddressSelectOption;
import org.mozilla.geckoview.Autocomplete.CreditCardSelectOption;
import org.mozilla.geckoview.Autocomplete.CreditCardSaveOption;
import org.mozilla.geckoview.Autocomplete.LoginSaveOption;
import org.mozilla.geckoview.Autocomplete.LoginSelectOption;
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest;
@ -451,6 +452,41 @@ import java.util.Map;
}
}
private static final class CreditCardSaveHandler
implements PromptHandler<AutocompleteRequest<CreditCardSaveOption>> {
@Override
public AutocompleteRequest<CreditCardSaveOption> newPrompt(
final GeckoBundle info,
final Observer observer) {
final int hint = info.getInt("hint");
final GeckoBundle[] creditCardBundles =
info.getBundleArray("creditCards");
if (creditCardBundles == null) {
return null;
}
final Autocomplete.CreditCardSaveOption[] options =
new Autocomplete.CreditCardSaveOption[creditCardBundles.length];
for (int i = 0; i < options.length; ++i) {
options[i] = new Autocomplete.CreditCardSaveOption(
new Autocomplete.CreditCard(creditCardBundles[i]),
hint);
}
return new PromptDelegate.AutocompleteRequest<>(info.getString("id"), options, observer);
}
@Override
public GeckoResult<PromptResponse> callDelegate(
final AutocompleteRequest<CreditCardSaveOption> prompt,
final GeckoSession session,
final PromptDelegate delegate) {
return delegate.onCreditCardSave(session, prompt);
}
}
private static final class AddressSaveHandler
implements PromptHandler<AutocompleteRequest<AddressSaveOption>> {
@Override
@ -612,6 +648,7 @@ import java.util.Map;
sPromptHandlers.register(new RepostHandler(), "repost");
sPromptHandlers.register(new ShareHandler(), "share");
sPromptHandlers.register(new LoginSaveHandler(), "Autocomplete:Save:Login");
sPromptHandlers.register(new CreditCardSaveHandler(), "Autocomplete:Save:CreditCard");
sPromptHandlers.register(new AddressSaveHandler(),
"Autocomplete:Save:Address");
sPromptHandlers.register(new LoginSelectHandler(),

Просмотреть файл

@ -48,6 +48,7 @@ import java.util.Date;
import java.util.Locale;
import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.Autocomplete;
import org.mozilla.geckoview.GeckoResult;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource;
@ -153,6 +154,15 @@ final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
return res;
}
@Nullable
@Override
public GeckoResult<PromptResponse> onCreditCardSave(
@NonNull GeckoSession session,
@NonNull AutocompleteRequest<Autocomplete.CreditCardSaveOption> request) {
Log.i(LOGTAG, "onCreditCardSave " + request.options[0].value);
return null;
}
@Nullable
@Override
public GeckoResult<PromptResponse> onBeforeUnloadPrompt(final GeckoSession session,

Просмотреть файл

@ -8,6 +8,7 @@ package org.mozilla.geckoview_example;
import org.json.JSONObject;
import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.Autocomplete;
import org.mozilla.geckoview.BasicSelectionActionDelegate;
import org.mozilla.geckoview.ContentBlocking;
import org.mozilla.geckoview.GeckoResult;
@ -747,6 +748,7 @@ public class GeckoViewActivity
mTabSessionManager.setTabObserver(sExtensionManager);
sGeckoRuntime.getWebExtensionController().setDebuggerDelegate(sExtensionManager);
sGeckoRuntime.setAutocompleteStorageDelegate(new ExampleAutocompleteStorageDelegate());
// `getSystemService` call requires API level 23
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -1529,6 +1531,8 @@ public class GeckoViewActivity
}
}
private class ExampleAutocompleteStorageDelegate implements Autocomplete.StorageDelegate {}
private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
@Override
public void onTitleChange(GeckoSession session, String title) {

Просмотреть файл

@ -375,7 +375,7 @@ const GeckoViewAutocomplete = {
* @param aCreditCard The {CreditCard} to be saved.
*/
onCreditCardSave(aCreditCard) {
debug`onLoginSave ${aCreditCard}`;
debug`onCreditCardSave ${aCreditCard}`;
EventDispatcher.instance.sendRequest({
type: "GeckoView:Autocomplete:Save:CreditCard",
@ -390,7 +390,7 @@ const GeckoViewAutocomplete = {
* @param aAddress The {Address} to be saved.
*/
onAddressSave(aAddress) {
debug`onLoginSave ${aAddress}`;
debug`onAddressSave ${aAddress}`;
EventDispatcher.instance.sendRequest({
type: "GeckoView:Autocomplete:Save:Address",

Просмотреть файл

@ -125,7 +125,7 @@ class FormAutofillSection {
*
*/
isRecordCreatable(record) {
throw new TypeError("isRecordCreatable method must be overrided");
throw new TypeError("isRecordCreatable method must be overridden");
}
/**

Просмотреть файл

@ -32,7 +32,7 @@ let FormAutofillPrompter = {
_createMessage(creditCards) {
let hint = CreditCardStorageHint.NONE;
return {
// Sync with GeckoSession.handlePromptEvent.
// Sync with PromptController
type: "Autocomplete:Save:CreditCard",
hint,
creditCards,