diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt index 97bb07dce9..6069da7639 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt @@ -5,13 +5,22 @@ package org.mozilla.fenix.settings.creditcards import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View import android.widget.ArrayAdapter import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import kotlinx.android.synthetic.main.fragment_credit_card_editor.view.* +import kotlinx.android.synthetic.main.fragment_credit_card_editor.* +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.creditcards.controller.CreditCardEditorController +import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardEditorController import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -21,23 +30,49 @@ import java.util.Locale */ class CreditCardEditorFragment : Fragment(R.layout.fragment_credit_card_editor) { + private lateinit var controller: CreditCardEditorController + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) showToolbar(getString(R.string.credit_cards_add_card)) - setupButtonClickListeners(view) + setHasOptionsMenu(true) + + setupButtonClickListeners() setupExpiryMonthDropDown(view) setupExpiryYearDropDown(view) + + controller = DefaultCreditCardEditorController( + storage = requireContext().components.core.autofillStorage, + lifecycleScope = lifecycleScope, + navController = findNavController() + ) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.credit_card_editor, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.save_credit_card_button -> { + saveCreditCard() + true + } + else -> false } /** * Setup the all button click listeners in the credit card editor. */ - private fun setupButtonClickListeners(view: View) { - view.cancel_button.setOnClickListener { + private fun setupButtonClickListeners() { + cancel_button.setOnClickListener { findNavController().popBackStack() } + + save_button.setOnClickListener { + saveCreditCard() + } } /** @@ -57,7 +92,7 @@ class CreditCardEditorFragment : Fragment(R.layout.fragment_credit_card_editor) adapter.add(dateFormat.format(calendar.time)) } - view.expiry_month_drop_down.adapter = adapter + expiry_month_drop_down.adapter = adapter } /** @@ -74,7 +109,25 @@ class CreditCardEditorFragment : Fragment(R.layout.fragment_credit_card_editor) adapter.add(year.toString()) } - view.expiry_year_drop_down.adapter = adapter + expiry_year_drop_down.adapter = adapter + } + + /** + * Helper function called by the the "Save" button and menu item to save a new credit card + * from the entered credit card fields. + */ + private fun saveCreditCard() { + view?.hideKeyboard() + + controller.handleSaveCreditCard( + UpdatableCreditCardFields( + billingName = name_on_card_input.text.toString(), + cardNumber = card_number_input.text.toString(), + expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(), + expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(), + cardType = "amex" + ) + ) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt new file mode 100644 index 0000000000..1f183c6ac3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards.controller + +import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage + +/** + * [CreditCardEditorFragment] controller. An interface that handles the view manipulation of the + * credit card editor. + */ +interface CreditCardEditorController { + + /** + * Saves the provided credit card field into the credit card storage. Called when a user + * taps on the save menu item or "Save" button. + * + * @param creditCardFields A [UpdatableCreditCardFields] record to add. + */ + fun handleSaveCreditCard(creditCardFields: UpdatableCreditCardFields) +} + +/** + * The default implementation of [CreditCardEditorController]. + * + * @param storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving + * credit cards. + * @param lifecycleScope [CoroutineScope] scope to launch coroutines. + * @param navController [NavController] used for navigation. + * @param ioDispatcher [CoroutineDispatcher] used for executing async tasks. Defaults to [Dispatchers.IO]. + */ +class DefaultCreditCardEditorController( + private val storage: AutofillCreditCardsAddressesStorage, + private val lifecycleScope: CoroutineScope, + private val navController: NavController, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : CreditCardEditorController { + + override fun handleSaveCreditCard(creditCardFields: UpdatableCreditCardFields) { + lifecycleScope.launch(ioDispatcher) { + storage.addCreditCard(creditCardFields) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } +} diff --git a/app/src/main/res/menu/credit_card_editor.xml b/app/src/main/res/menu/credit_card_editor.xml new file mode 100644 index 0000000000..d5340e566c --- /dev/null +++ b/app/src/main/res/menu/credit_card_editor.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/test/java/org/mozilla/fenix/settings/creditcards/DefaultCreditCardEditorControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/creditcards/DefaultCreditCardEditorControllerTest.kt new file mode 100644 index 0000000000..440be45113 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/creditcards/DefaultCreditCardEditorControllerTest.kt @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards + +import androidx.navigation.NavController +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardEditorController + +@ExperimentalCoroutinesApi +class DefaultCreditCardEditorControllerTest { + + private val storage: AutofillCreditCardsAddressesStorage = mockk(relaxed = true) + private val navController: NavController = mockk(relaxed = true) + + private val testCoroutineScope = TestCoroutineScope() + private val testDispatcher = TestCoroutineDispatcher() + + private lateinit var controller: DefaultCreditCardEditorController + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + + @Before + fun setup() { + controller = spyk( + DefaultCreditCardEditorController( + storage = storage, + lifecycleScope = testCoroutineScope, + navController = navController, + ioDispatcher = testDispatcher + ) + ) + } + + @After + fun cleanUp() { + testCoroutineScope.cleanupTestCoroutines() + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun handleSaveCreditCard() = testCoroutineScope.runBlockingTest { + val creditCardFields = UpdatableCreditCardFields( + billingName = "Banana Apple", + cardNumber = "4111111111111112", + expiryMonth = 1, + expiryYear = 2030, + cardType = "discover" + ) + + controller.handleSaveCreditCard(creditCardFields) + + coVerify { + storage.addCreditCard(creditCardFields) + navController.popBackStack() + } + } +}