diff --git a/app/build.gradle b/app/build.gradle index c034f02..95c62a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,6 +16,10 @@ android { versionName "1.0." + versionCode } + buildFeatures { + viewBinding = true + } + buildTypes { release { minifyEnabled false @@ -52,4 +56,6 @@ dependencies { kapt "com.google.dagger:dagger-android-processor:$dagger2Version" implementation project(path: ':sdk') + + implementation "androidx.navigation:navigation-fragment-ktx:2.5.3" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2a7d72e..79b16f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,8 +14,9 @@ android:theme="@style/Theme.PaymentsSdk" tools:targetApi="31"> + android:name="payselection.demo.ui.MainActivity" + android:exported="true" + android:screenOrientation="sensorPortrait"> diff --git a/app/src/main/java/payselection/demo/MainActivity.kt b/app/src/main/java/payselection/demo/MainActivity.kt deleted file mode 100644 index becdfc3..0000000 --- a/app/src/main/java/payselection/demo/MainActivity.kt +++ /dev/null @@ -1,121 +0,0 @@ -package payselection.demo - -import android.annotation.SuppressLint -import android.os.Bundle -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import payselection.payments.sdk.PaySelectionPaymentsSdk -import payselection.payments.sdk.configuration.SdkConfiguration -import payselection.payments.sdk.models.requests.pay.* -import payselection.payments.sdk.ui.ThreeDsDialogFragment - - -class MainActivity : AppCompatActivity(), ThreeDsDialogFragment.ThreeDSDialogListener { - - lateinit var sdk: PaySelectionPaymentsSdk - - private val handler = CoroutineExceptionHandler { context, exception -> - runOnUiThread { - Toast.makeText(application, "Caught $exception", Toast.LENGTH_LONG).show() - } - } - - @SuppressLint("MissingInflatedId") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - sdk = PaySelectionPaymentsSdk.getInstance( - SdkConfiguration( - "04bd07d3547bd1f90ddbd985feaaec59420cabd082ff5215f34fd1c89c5d8562e8f5e97a5df87d7c99bc6f16a946319f61f9eb3ef7cf355d62469edb96c8bea09e", - "21044", - true - ) - ) - - makePay("5260111696757102") - } - - private fun getTransaction() { - GlobalScope.launch(handler) { - //Get this properties from PaymentResult - val transactionId = "PS00000300026126" - val transactionKey = "d58f99b6-6c6d-4186-9727-7ee5115ca288" - testGetTransaction(transactionKey, transactionId) - } - } - - private fun makePay(cardNumber: String) { - GlobalScope.launch(handler) { - val orderId = "SAM_SDK_3" - testPay(orderId, cardNumber) - } - } - - suspend fun testPay(orderId: String, cardNumber: String) { - sdk.pay( - orderId = orderId, - description = "test payment", - paymentData = PaymentData.create( - transactionDetails = TransactionDetails( - amount = "10", - currency = "RUB" - ), - cardDetails = CardDetails( - cardholderName = "TEST CARD", - cardNumber = cardNumber, - cvc = "123", - expMonth = "12", - expYear = "24" - ) - ), - customerInfo = CustomerInfo( - email = "user@example.com", - phone = "+19991231212", - language = "en", - address = "string", - town = "string", - zip = "string", - country = "USA" - ), - rebillFlag = false - ).proceedResult( - success = { - println("Result $it") - show3DS(it.redirectUrl) - }, - error = { - it.printStackTrace() - } - ) - } - - suspend fun testGetTransaction(transactionKey: String, transactionId: String) { - sdk.getTransaction(transactionKey, transactionId).proceedResult( - success = { - println("Result $it") - }, - error = { - it.printStackTrace() - } - ) - } - - private fun show3DS(url: String) { - // Открываем 3ds форму - ThreeDsDialogFragment - .newInstance(url) - .show(supportFragmentManager, "3DS") - } - - override fun onAuthorizationCompleted() { - Toast.makeText(application, "Success", Toast.LENGTH_LONG).show() - } - - override fun onAuthorizationFailed() { - Toast.makeText(application, "Fail", Toast.LENGTH_LONG).show() - } -} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/models/Card.kt b/app/src/main/java/payselection/demo/models/Card.kt new file mode 100644 index 0000000..447d61e --- /dev/null +++ b/app/src/main/java/payselection/demo/models/Card.kt @@ -0,0 +1,7 @@ +package payselection.demo.models + +data class Card( + val number: String, + val date: String, + var cvv: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/models/Product.kt b/app/src/main/java/payselection/demo/models/Product.kt new file mode 100644 index 0000000..c92edad --- /dev/null +++ b/app/src/main/java/payselection/demo/models/Product.kt @@ -0,0 +1,8 @@ +package payselection.demo.models + +data class Product( + val name: String, + val description: String, + val price: String, + val image: Int +) \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/sdk/PaymentService.kt b/app/src/main/java/payselection/demo/sdk/PaymentService.kt new file mode 100644 index 0000000..af977af --- /dev/null +++ b/app/src/main/java/payselection/demo/sdk/PaymentService.kt @@ -0,0 +1,114 @@ +package payselection.demo.sdk + +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import payselection.demo.models.Card +import payselection.demo.ui.checkout.common.PaymentResultListener +import payselection.payments.sdk.PaySelectionPaymentsSdk +import payselection.payments.sdk.configuration.SdkConfiguration +import payselection.payments.sdk.models.requests.pay.CardDetails +import payselection.payments.sdk.models.requests.pay.CustomerInfo +import payselection.payments.sdk.models.requests.pay.PaymentData +import payselection.payments.sdk.models.requests.pay.TransactionDetails +import payselection.payments.sdk.models.results.pay.PaymentResult + +class PaymentService { + + private var sdk: PaySelectionPaymentsSdk? = null + var paymentResult: PaymentResult? = null + var card: Card? = null + + private var paymentResultListener: PaymentResultListener? = null + + fun init(sdkConfiguration: SdkConfiguration) { + sdk = PaySelectionPaymentsSdk.getInstance(sdkConfiguration) + } + + private val handler = CoroutineExceptionHandler { context, exception -> + Log.e("SdkHelper", "Caught $exception") + } + + fun pay(paymentCard: Card) { + CoroutineScope(Dispatchers.IO).launch(handler) { + card = paymentCard + val orderId = "SAM_SDK_3" + testPay(orderId, paymentCard) + } + } + + private suspend fun testPay(orderId: String, card: Card) { + val dateParts = card.date.split('/') + sdk?.pay( + orderId = orderId, + description = "test payment", + paymentData = PaymentData.create( + transactionDetails = TransactionDetails( + amount = "10", + currency = "RUB" + ), + cardDetails = CardDetails( + cardholderName = "TEST CARD", + cardNumber = card.number, + cvc = card.cvv.orEmpty(), + expMonth = dateParts[0], + expYear = dateParts[1] + ) + ), + customerInfo = CustomerInfo( + email = "user@example.com", + phone = "+19991231212", + language = "en", + address = "string", + town = "string", + zip = "string", + country = "USA" + ), + rebillFlag = false + )?.proceedResult( + success = { + paymentResult = it + paymentResultListener?.onPaymentResult(it) + }, + error = { + paymentResultListener?.onPaymentResult(null) + it.printStackTrace() + } + ) + } + + fun getTransaction() { + CoroutineScope(Dispatchers.IO).launch(handler) { + //Get this properties from PaymentResult + val transactionId = paymentResult?.transactionId + val transactionKey = paymentResult?.transactionSecretKey + testGetTransaction(transactionKey.orEmpty(), transactionId.orEmpty()) + } + } + + private suspend fun testGetTransaction(transactionKey: String, transactionId: String) { + sdk?.getTransaction(transactionKey, transactionId)?.proceedResult( + success = { + println("Result $it") + }, + error = { + it.printStackTrace() + } + ) + } + + companion object { + private var instance: PaymentService? = null + + fun getInstance(paymentResultListener: PaymentResultListener? = null): PaymentService { + if (instance == null) { + instance = PaymentService() + } + if (paymentResultListener != null) instance?.paymentResultListener = paymentResultListener + return instance ?: throw IllegalStateException("Unable to create instance.") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/MainActivity.kt b/app/src/main/java/payselection/demo/ui/MainActivity.kt new file mode 100644 index 0000000..173495c --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/MainActivity.kt @@ -0,0 +1,45 @@ +package payselection.demo.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import payselection.demo.R +import payselection.demo.databinding.ActivityMainBinding +import payselection.demo.ui.checkout.CheckoutFragment +import payselection.demo.ui.result.ResultFragment +import payselection.demo.ui.result.ResultFragment.Companion.ARG_IS_SUCCESS +import payselection.payments.sdk.ui.ThreeDsDialogFragment + + +class MainActivity : AppCompatActivity(), ThreeDsDialogFragment.ThreeDSDialogListener { + + private lateinit var viewBinding: ActivityMainBinding + + @SuppressLint("MissingInflatedId") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(viewBinding.root) + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.fragment_container, CheckoutFragment()) + fragmentTransaction.commit() + } + + override fun onAuthorizationCompleted() { + val bundle = Bundle() + bundle.putBoolean(ARG_IS_SUCCESS, true) + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.fragment_container, ResultFragment.createInstance(bundle)) + .addToBackStack(ResultFragment::class.java.canonicalName) + fragmentTransaction.commit() + } + + override fun onAuthorizationFailed() { + val bundle = Bundle() + bundle.putBoolean(ARG_IS_SUCCESS, false) + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.fragment_container, ResultFragment.createInstance(bundle)) + .addToBackStack(ResultFragment::class.java.canonicalName) + fragmentTransaction.commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/CheckoutFragment.kt b/app/src/main/java/payselection/demo/ui/checkout/CheckoutFragment.kt new file mode 100644 index 0000000..62d33e0 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/CheckoutFragment.kt @@ -0,0 +1,85 @@ +package payselection.demo.ui.checkout + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import payselection.demo.R +import payselection.demo.databinding.FCheckoutBinding +import payselection.demo.models.Product +import payselection.demo.ui.checkout.adapter.ProductAdapter +import payselection.demo.ui.checkout.buttomSheet.BottomSheetPay + +class CheckoutFragment : Fragment() { + + private lateinit var viewBinding: FCheckoutBinding + private lateinit var productsAdapter: ProductAdapter + + private val viewModel: CheckoutViewModel by activityViewModels() + + private val products by lazy { + listOf( + Product( + name = resources.getString(R.string.product_1_name), + description = resources.getString(R.string.product_1_desc), + price = resources.getString(R.string.product_1_price), + image = R.drawable.image_card_1 + ), + Product( + name = resources.getString(R.string.product_2_name), + description = resources.getString(R.string.product_2_desc), + price = resources.getString(R.string.product_2_price), + image = R.drawable.image_card_2 + ), + Product( + name = resources.getString(R.string.product_3_name), + description = resources.getString(R.string.product_3_desc), + price = resources.getString(R.string.price_149), + image = R.drawable.image_card_3 + ) + ) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + viewBinding = FCheckoutBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + configureProducts() + configureButton() + } + + override fun onResume() { + super.onResume() + viewBinding.pay.isEnabled = true + } + + private fun configureProducts() { + productsAdapter = ProductAdapter(products) + viewBinding.cards.adapter = productsAdapter + viewBinding.cards.layoutManager = LinearLayoutManager(requireContext()) + } + + private fun configureButton() { + viewModel.checkoutButtonEnable.observe(viewLifecycleOwner){ + viewBinding.pay.isEnabled = it + } + viewBinding.pay.setOnClickListener { + BottomSheetPay().show(requireActivity().supportFragmentManager, BottomSheetPay::class.java.canonicalName) + viewModel.updateCheckoutButtonEnable(false) + } + } + + override fun onPause() { + super.onPause() + println("on pause") + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/CheckoutViewModel.kt b/app/src/main/java/payselection/demo/ui/checkout/CheckoutViewModel.kt new file mode 100644 index 0000000..2357ea2 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/CheckoutViewModel.kt @@ -0,0 +1,130 @@ +package payselection.demo.ui.checkout + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import payselection.demo.models.Card +import payselection.demo.ui.checkout.common.ActionState +import payselection.demo.utils.ADD_ITEM_INDEX +import payselection.demo.utils.CARD_CVV_LENGTH +import payselection.demo.utils.CARD_DATE_LENGTH +import payselection.demo.utils.CARD_NUMBER_LENGTH +import payselection.demo.utils.CombineTripleLiveData +import payselection.demo.utils.isValidCardDate +import payselection.demo.utils.isValidCardNumber +import payselection.demo.utils.isValidCvv + +class CheckoutViewModel : ViewModel() { + + private val _cards = MutableLiveData>() + val cards: LiveData> = _cards + + var currentPosition = MutableLiveData>(Pair(null, false)) + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _isNumberValid = MutableLiveData() + val isNumberValid: LiveData = _isNumberValid + + private val _isDataValid = MutableLiveData() + val isDataValid: LiveData = _isDataValid + + private val _isCvvValid = MutableLiveData() + val isCvvValid: LiveData = _isCvvValid + + var cardDate = "" + var cardNumber = "" + var cardCvv = "" + + private val _checkoutButtonEnable = MutableLiveData() + val checkoutButtonEnable: LiveData = _checkoutButtonEnable + + val isEnable = CombineTripleLiveData(_isDataValid, _isNumberValid, _isCvvValid) { isDataValid, isNumberValid, isCvvValid -> + isDataValid == true && isNumberValid == true && isCvvValid == true && cardDate.length == CARD_DATE_LENGTH && cardNumber.length == CARD_NUMBER_LENGTH + && cardCvv.length == CARD_CVV_LENGTH + } + + val actionState: LiveData = Transformations.map(currentPosition) { + if (it.first == null || it.first == ADD_ITEM_INDEX) ActionState.ADD else ActionState.PAY + } + + fun onCardSelected(cardIndex: Int) { + currentPosition.postValue( + Pair( + if (cardIndex == (cards.value?.size ?: 0)) ADD_ITEM_INDEX else cardIndex, + false + ) + ) + } + + fun addCard(number: String, date: String) { + val card = Card(number, date) + val cards = _cards.value ?: emptyList() + _cards.value = cards.toMutableList().apply { add(card) } + currentPosition.postValue(Pair(cards.size, true)) + } + + fun putCardDate(date: String) { + this.cardDate = date + validCardDate() + } + + fun putCardCvv(cvv: String) { + cardCvv = cvv + } + + fun putCardNumber(cardNumber: String) { + this.cardNumber = cardNumber + validCardNumber() + } + + fun validCardNumber(hasFocus: Boolean = true) { + if (hasFocus) { + if (cardNumber.length == CARD_NUMBER_LENGTH) { + _isNumberValid.postValue(isValidCardNumber(cardNumber)) + } else { + _isNumberValid.postValue(true) + } + } else { + _isNumberValid.postValue(isValidCardNumber(cardNumber)) + } + } + + fun validCardDate(hasFocus: Boolean = true) { + if (hasFocus) { + if (cardDate.length == CARD_DATE_LENGTH) { + _isDataValid.postValue(isValidCardDate(cardDate)) + } else { + _isDataValid.postValue(true) + } + } else { + _isDataValid.postValue(isValidCardDate(cardDate)) + } + } + + fun validCardCvv(hasFocus: Boolean = true) { + if (hasFocus) { + _isCvvValid.postValue(true) + } else { + _isCvvValid.postValue(isValidCvv(cardCvv)) + } + } + + fun replaceCard(newCard: Card) { + val cards = _cards.value?.toMutableList() ?: return + val position = currentPosition.value?.first ?: return + + cards[position] = newCard + _cards.postValue(cards) + } + + fun updateLoad(isLoad: Boolean) { + _isLoading.postValue(isLoad) + } + + fun updateCheckoutButtonEnable(isEnable: Boolean) { + _checkoutButtonEnable.postValue(isEnable) + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/adapter/CardAdapter.kt b/app/src/main/java/payselection/demo/ui/checkout/adapter/CardAdapter.kt new file mode 100644 index 0000000..32362af --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/adapter/CardAdapter.kt @@ -0,0 +1,97 @@ +package payselection.demo.ui.checkout.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import payselection.demo.R +import payselection.demo.databinding.ICardBinding +import payselection.demo.models.Card +import payselection.demo.ui.checkout.common.CardListener +import payselection.demo.utils.ADD_ITEM_INDEX +import payselection.demo.utils.getPaymentSystem + + +class CardAdapter(private val cardListener: CardListener) : + RecyclerView.Adapter() { + + private var selectedIndex: Int? = null + private var list: List = emptyList() + + inner class CardHolder(private val view: ICardBinding) : RecyclerView.ViewHolder(view.root) { + init { + view.root.setOnClickListener { + cardListener.onSelect(adapterPosition) + } + } + + fun bind(card: Card, isSelected: Boolean) { + with(view) { + cardNumber.text = root.context.getString(R.string.card_number_format, card.number.takeLast(4)) + bgCardType.setBackgroundResource(R.drawable.bg_card_type) + cardNumber.textSize = 12F + val paymentSystem = getPaymentSystem(card.number.filter { it.isDigit() }) + if (paymentSystem != null) { + imageCardType.setImageResource(paymentSystem.image) + } else { + imageCardType.setImageDrawable(null) + } + imageAdd.setImageResource(R.drawable.ic_ready) + if (isSelected) { + cardNumber.setTextColor(ContextCompat.getColor(root.context, R.color.white)) + root.setBackgroundResource(R.drawable.bg_select_card) + } else { + cardNumber.setTextColor(ContextCompat.getColor(root.context, R.color.gray)) + root.setBackgroundResource(R.drawable.bg_card) + } + } + } + + fun bindAddCard(isSelected: Boolean) { + with(view) { + cardNumber.text = root.context.getString(R.string.card_adding) + imageCardType.setImageDrawable(null) + cardNumber.textSize = 10F + bgCardType.setBackgroundResource(R.drawable.bg_card_type_add) + cardNumber.setTextColor(ContextCompat.getColor(root.context, R.color.gray)) + if (isSelected) { + root.setBackgroundResource(R.drawable.bg_card_add_selected) + imageAdd.setImageResource(R.drawable.ic_ready_blue) + } else { + root.setBackgroundResource(R.drawable.bg_card) + imageAdd.setImageResource(R.drawable.ic_plus) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardHolder { + return CardHolder( + ICardBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: CardHolder, position: Int) { + if (position < list.size) { + holder.bind(list[position], selectedIndex == position) + } else { + holder.bindAddCard(selectedIndex == ADD_ITEM_INDEX) + } + } + + override fun getItemCount() = list.size + 1 + + fun updatePosition(index: Int?) { + selectedIndex = index + notifyDataSetChanged() + } + + fun updateList(list: List) { + this.list = list + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/adapter/ProductAdapter.kt b/app/src/main/java/payselection/demo/ui/checkout/adapter/ProductAdapter.kt new file mode 100644 index 0000000..a2521d7 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/adapter/ProductAdapter.kt @@ -0,0 +1,37 @@ +package payselection.demo.ui.checkout.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import payselection.demo.databinding.IProductBinding +import payselection.demo.models.Product + +class ProductAdapter(private val items: List) : RecyclerView.Adapter() { + + inner class ProductHolder(private val view: IProductBinding) : RecyclerView.ViewHolder(view.root) { + fun bind(item: Product) { + with(view) { + title.text = item.name + disc.text = item.description + price.text = item.price + image.setImageResource(item.image) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductHolder { + return ProductHolder( + IProductBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ProductHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount() = items.size +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/buttomSheet/BottomSheetPay.kt b/app/src/main/java/payselection/demo/ui/checkout/buttomSheet/BottomSheetPay.kt new file mode 100644 index 0000000..e62404a --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/buttomSheet/BottomSheetPay.kt @@ -0,0 +1,243 @@ +package payselection.demo.ui.checkout.buttomSheet + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import payselection.demo.R +import payselection.demo.databinding.ButtomSheetBinding +import payselection.demo.models.Card +import payselection.demo.sdk.PaymentService +import payselection.demo.ui.checkout.CheckoutViewModel +import payselection.demo.ui.checkout.adapter.CardAdapter +import payselection.demo.ui.checkout.common.ActionState +import payselection.demo.ui.checkout.common.CardListener +import payselection.demo.ui.checkout.common.PaymentResultListener +import payselection.demo.ui.result.ResultFragment +import payselection.demo.ui.result.ResultFragment.Companion.ARG_IS_SUCCESS +import payselection.demo.utils.ADD_ITEM_INDEX +import payselection.demo.utils.EMPTY_STRING +import payselection.demo.utils.ExpiryDateTextWatcher +import payselection.demo.utils.FourDigitCardFormatWatcher +import payselection.demo.utils.ThreeDigitWatcher +import payselection.demo.utils.getPaymentSystem +import payselection.demo.utils.updateColor +import payselection.payments.sdk.configuration.SdkConfiguration +import payselection.payments.sdk.models.results.pay.PaymentResult +import payselection.payments.sdk.ui.ThreeDsDialogFragment + + +class BottomSheetPay : BottomSheetDialogFragment(), CardListener, PaymentResultListener { + + private lateinit var binding: ButtomSheetBinding + private val viewModel: CheckoutViewModel by activityViewModels() + + private lateinit var cardsAdapter: CardAdapter + + private lateinit var paymentService: PaymentService + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.AppBottomSheetDialogTheme); + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ButtomSheetBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + configureCardAdapter() + configureError() + configureButton() + configureAnother() + } + + private fun configureCardAdapter() = with(binding) { + cardsAdapter = CardAdapter(this@BottomSheetPay) + cardsPager.adapter = cardsAdapter + cardsPager.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + viewModel.cards.observe(viewLifecycleOwner) { + cardsAdapter.updateList(it) + } + } + + private fun configureButton() { + with(binding) { + viewModel.isEnable.observe(viewLifecycleOwner) { + pay.isEnabled = it + } + viewModel.actionState.observe(viewLifecycleOwner) { state -> + pay.text = + if (state == ActionState.PAY) requireContext().getString(R.string.pay_card) else requireContext().getString(R.string.save_card) + pay.setOnClickListener { + when (state) { + ActionState.ADD -> viewModel.addCard(editCardNumber.text.toString(), editCardData.text.toString()) + else -> { + val payCard = Card(viewModel.cardNumber, viewModel.cardDate) + viewModel.replaceCard(payCard) + pay(payCard.apply { cvv = editCardCvv.text.toString()}) + } + } + } + } + } + } + + private fun configureAnother() { + with(binding) { + editCardNumber.addTextChangedListener(FourDigitCardFormatWatcher()) + editCardData.addTextChangedListener(ExpiryDateTextWatcher()) + editCardCvv.addTextChangedListener(ThreeDigitWatcher()) + + val typeface = ResourcesCompat.getFont(requireContext(), R.font.raleway_500) + cardNumber.typeface = typeface + cardData.typeface = typeface + cardCvv.typeface = typeface + + viewModel.currentPosition.observe(viewLifecycleOwner) { currentPosition -> + val position = currentPosition.first + cardsPager.smoothScrollToPosition((if (position == -1) cardsPager.adapter?.itemCount ?: 0 else (position?:0))) + + if (position == ADD_ITEM_INDEX || position == null) { + editCardNumber.setText(EMPTY_STRING) + editCardData.setText(EMPTY_STRING) + editCardCvv.setText(EMPTY_STRING) + } else { + val cards = viewModel.cards.value + editCardNumber.setText(cards?.get(position)?.number.orEmpty()) + editCardData.setText(cards?.get(position)?.date.orEmpty()) + if (position != (cards?.size?.minus(1)) || currentPosition.second.not()) editCardCvv.setText(EMPTY_STRING) + } + requireView().findFocus()?.clearFocus() + cardsAdapter.updatePosition(position) + } + + viewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> + pay.isEnabled = isLoading.not() + } + } + } + + private fun configureError() { + with(binding) { + editCardCvv.doOnTextChanged { text, _, _, _ -> + viewModel.putCardCvv(text.toString()) + viewModel.validCardCvv(editCardCvv.hasFocus()) + cardCvv.isEndIconVisible = text?.isEmpty() != true + } + + editCardCvv.setOnFocusChangeListener { _, hasFocus -> + viewModel.validCardCvv(hasFocus = hasFocus) + } + editCardNumber.setOnFocusChangeListener { _, hasFocus -> + viewModel.validCardNumber(hasFocus = hasFocus) + } + editCardData.setOnFocusChangeListener { _, hasFocus -> + viewModel.validCardDate(hasFocus = hasFocus) + } + + editCardData.doOnTextChanged { text, _, _, _ -> + viewModel.putCardDate(text.toString()) + } + + editCardNumber.doOnTextChanged { text, _, _, _ -> + val cardNumber = text.toString().replace(" ", "") + viewModel.putCardNumber(cardNumber) + binding.cardNumber.endIconDrawable = getPaymentSystem(cardNumber)?.let { paymentSystem -> + ContextCompat.getDrawable(requireContext(), paymentSystem.imageWithLine) + } + } + + viewModel.isCvvValid.observe(viewLifecycleOwner) { isValid -> + cardCvv.updateColor( + requireContext(), + !isValid, + requireContext().getString(R.string.cvv), + requireContext().getString(R.string.error_cvv) + ) + editCardCvv.updateColor(requireContext(), !isValid) + } + + viewModel.isDataValid.observe(viewLifecycleOwner) { isValid -> + cardData.updateColor( + requireContext(), + !isValid, + requireContext().getString(R.string.dd_mm), + requireContext().getString(R.string.error_date) + ) + editCardData.updateColor(requireContext(), !isValid) + } + + viewModel.isNumberValid.observe(viewLifecycleOwner) { isValid -> + cardNumber.updateColor( + requireContext(), + !isValid, + requireContext().getString(R.string.card_number), + requireContext().getString(R.string.error_number) + ) + editCardNumber.updateColor(requireContext(), !isValid) + cardNumber.isEndIconVisible = isValid && viewModel.cardNumber.isNotEmpty() + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + binding.editCardNumber.setText(EMPTY_STRING) + binding.editCardData.setText(EMPTY_STRING) + binding.editCardCvv.setText(EMPTY_STRING) + viewModel.updateCheckoutButtonEnable(true) + } + + private fun pay(card: Card) { + viewModel.updateLoad(true) + paymentService = PaymentService.getInstance(this) + paymentService.init( + SdkConfiguration( + "04bd07d3547bd1f90ddbd985feaaec59420cabd082ff5215f34fd1c89c5d8562e8f5e97a5df87d7c99bc6f16a946319f61f9eb3ef7cf355d62469edb96c8bea09e", + "21044", + true + ) + ) + paymentService.pay(card) + } + + private fun show3DS(url: String) { + // Открываем 3ds форму + ThreeDsDialogFragment + .newInstance(url) + .show(requireActivity().supportFragmentManager, "3DS") + } + + override fun onSelect(position: Int) { + viewModel.onCardSelected(position) + } + + override fun onPaymentResult(result: PaymentResult?) { + dismiss() + viewModel.updateLoad(true) + if (result != null) { + show3DS(result.redirectUrl) + } else { + val bundle = Bundle() + bundle.putBoolean(ARG_IS_SUCCESS, false) + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + fragmentTransaction.add(R.id.fragment_container, ResultFragment.createInstance(bundle)) + .addToBackStack(ResultFragment::class.java.canonicalName) + fragmentTransaction.commit() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/common/ActionState.kt b/app/src/main/java/payselection/demo/ui/checkout/common/ActionState.kt new file mode 100644 index 0000000..fa1a103 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/common/ActionState.kt @@ -0,0 +1,6 @@ +package payselection.demo.ui.checkout.common + +enum class ActionState { + ADD, + PAY +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/common/CardListener.kt b/app/src/main/java/payselection/demo/ui/checkout/common/CardListener.kt new file mode 100644 index 0000000..069ecaa --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/common/CardListener.kt @@ -0,0 +1,5 @@ +package payselection.demo.ui.checkout.common + +interface CardListener { + fun onSelect(position: Int) +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/common/CardType.kt b/app/src/main/java/payselection/demo/ui/checkout/common/CardType.kt new file mode 100644 index 0000000..81cdcbb --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/common/CardType.kt @@ -0,0 +1,9 @@ +package payselection.demo.ui.checkout.common + +import payselection.demo.R + +enum class CardType(val title:Int, val image: Int, val imageWithLine: Int) { + VISA(R.string.visa, R.drawable.ic_visa, R.drawable.ic_visa_with_line), + MASTERCARD(R.string.mastercard, R.drawable.ic_mastercard, R.drawable.ic_mastercard_with_line), + MIR(R.string.mir, R.drawable.ic_mir, R.drawable.ic_mir_with_line) +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/checkout/common/PaymentResultListener.kt b/app/src/main/java/payselection/demo/ui/checkout/common/PaymentResultListener.kt new file mode 100644 index 0000000..50e6d93 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/checkout/common/PaymentResultListener.kt @@ -0,0 +1,7 @@ +package payselection.demo.ui.checkout.common + +import payselection.payments.sdk.models.results.pay.PaymentResult + +interface PaymentResultListener { + fun onPaymentResult(result: PaymentResult?) +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/ui/result/ResultFragment.kt b/app/src/main/java/payselection/demo/ui/result/ResultFragment.kt new file mode 100644 index 0000000..e59f0c5 --- /dev/null +++ b/app/src/main/java/payselection/demo/ui/result/ResultFragment.kt @@ -0,0 +1,71 @@ +package payselection.demo.ui.result + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import payselection.demo.R +import payselection.demo.databinding.FResultBinding +import payselection.demo.sdk.PaymentService +import payselection.demo.utils.EMPTY_STRING +import payselection.demo.utils.getPaymentSystem + +class ResultFragment : Fragment() { + private lateinit var viewBinding: FResultBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + viewBinding = FResultBinding.inflate(inflater, container, false) + return viewBinding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + configureUI() + } + + private fun configureUI() { + val success = arguments?.getBoolean(ARG_IS_SUCCESS) ?: false + if (success) configureSuccessUI() else configureErrorUI() + + viewBinding.navButton.setOnClickListener { + val fragmentManager = requireActivity().supportFragmentManager + fragmentManager.popBackStack() + } + } + + private fun configureSuccessUI() { + with(viewBinding) { + success.visibility = View.VISIBLE + error.visibility = View.GONE + configurePaymentUI() + } + } + + private fun configureErrorUI() { + with(viewBinding) { + success.visibility = View.GONE + error.visibility = View.VISIBLE + close.setOnClickListener { + requireActivity().supportFragmentManager.popBackStack() + } + } + } + + private fun configurePaymentUI() { + val paymentHelper = PaymentService.getInstance() + val cardNumber = paymentHelper.card?.number ?: EMPTY_STRING + viewBinding.payCard.text = + resources.getString(R.string.paid_card_format, getString(getPaymentSystem(cardNumber)?.title ?: R.string.unknown), cardNumber.takeLast(4)) + } + + companion object { + fun createInstance(bundle: Bundle): ResultFragment { + val fragment = ResultFragment() + fragment.arguments = bundle + return fragment + } + + const val ARG_IS_SUCCESS = "is_success" + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/CardUtils.kt b/app/src/main/java/payselection/demo/utils/CardUtils.kt new file mode 100644 index 0000000..dbd5178 --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/CardUtils.kt @@ -0,0 +1,63 @@ +package payselection.demo.utils + +import payselection.demo.ui.checkout.CheckoutViewModel +import payselection.demo.ui.checkout.common.CardType +import java.util.Calendar + +fun String.luhnAlgorithm() = reversed() + .map(Character::getNumericValue) + .mapIndexed { index, digit -> + when { + index % 2 == 0 -> digit + digit < 5 -> digit * 2 + else -> digit * 2 - 9 + } + }.sum() % 10 == 0 + +fun isValidCardNumber(cardNumber: String): Boolean { + return if (cardNumber.isEmpty()) { + true + } else if (cardNumber.length != CARD_NUMBER_LENGTH) { + false + } else { + cardNumber.luhnAlgorithm() + } +} + +fun isValidCardDate(date: String): Boolean { + val isValid = + if (date.length == 5 && date.indexOf('/') == 2) { + val month = date.substring(0, 2).toInt() + val year = date.substring(3).toInt() + val allowedDate = Calendar.getInstance() + allowedDate.set(2022, 1, 1) + val inputDate = Calendar.getInstance() + inputDate.set(2000 + year, month, 1) + + month in 1..12 && inputDate >= allowedDate + } else { + false + } + return isValid || date.isEmpty() +} + +fun isValidCvv(cvv: String): Boolean { + return cvv.length == CARD_CVV_LENGTH || cvv.isEmpty() +} + +fun getPaymentSystem(cardNumber: String): CardType? = when { + cardNumber.startsWith("4") -> CardType.VISA + cardNumber.startsWith("51") || cardNumber.startsWith("52") || cardNumber.startsWith("53") + || cardNumber.startsWith("54") || cardNumber.startsWith("55") -> CardType.MASTERCARD + + cardNumber.startsWith("2") -> CardType.MIR + else -> null +} + +val MASTERCARD_REGEX = Regex("^5[1-5][0-9]{14}$") +val VISA_REGEX = Regex("^4[0-9]{12}(?:[0-9]{3})?$") +val MIR_REGEX = Regex("^2[0-9]{15}$") + +val CARD_NUMBER_LENGTH = 16 +val CARD_DATE_LENGTH = 5 +val CARD_CVV_LENGTH = 3 \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/CombineLiveData.kt b/app/src/main/java/payselection/demo/utils/CombineLiveData.kt new file mode 100644 index 0000000..378b636 --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/CombineLiveData.kt @@ -0,0 +1,19 @@ +package payselection.demo.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +class CombineTripleLiveData(first: LiveData, second: LiveData, third: LiveData, combine: (F?, S?, T?) -> R) : MediatorLiveData() { + + init { + addSource(first) { + value = combine(it, second.value, third.value) + } + addSource(second) { + value = combine(first.value, it, third.value) + } + addSource(third) { + value = combine(first.value, second.value, it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/ExpiryDateTextWatcher.kt b/app/src/main/java/payselection/demo/utils/ExpiryDateTextWatcher.kt new file mode 100644 index 0000000..88db61d --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/ExpiryDateTextWatcher.kt @@ -0,0 +1,77 @@ +package payselection.demo.utils + +import android.text.Editable +import android.text.TextWatcher + + +class ExpiryDateTextWatcher : TextWatcher { + + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun afterTextChanged(s: Editable) { + if (!isInputCorrect( + s, + TOTAL_SYMBOLS, + DIVIDER_INDEX, + DIVIDER + ) + ) { + s.replace(0, s.length, getDigitArray(s, TOTAL_DIGITS)?.let { + buildCorrectString( + it, + DIVIDER_POSITION, + DIVIDER + ) + }); + } + } + + private fun isInputCorrect(s: Editable, totalSymbols: Int, dividerModulo: Int, divider: Char): Boolean { + var isCorrect = s.length <= totalSymbols // check size of entered string + for (i in s.indices) { // check that every element is right + isCorrect = if (i > 0 && (i + 1) % dividerModulo == 0) { + isCorrect and (divider == s[i]) + } else { + isCorrect and Character.isDigit(s[i]) + } + } + return isCorrect + } + + private fun buildCorrectString(digits: CharArray, dividerPosition: Int, divider: Char): String? { + val formatted = StringBuilder() + for (i in digits.indices) { + if (digits[i].code != 0) { + formatted.append(digits[i]) + if (i > 0 && i < digits.size - 1 && (i + 1) % dividerPosition == 0) { + formatted.append(divider) + } + } + } + return formatted.toString() + } + + private fun getDigitArray(s: Editable, size: Int): CharArray? { + val digits = CharArray(size) + var index = 0 + var i = 0 + while (i < s.length && index < size) { + val current = s[i] + if (Character.isDigit(current)) { + digits[index] = current + index++ + } + i++ + } + return digits + } + + companion object { + + private const val DIVIDER = '/' + private const val TOTAL_SYMBOLS = 5 + private const val TOTAL_DIGITS = 4 + private const val DIVIDER_INDEX = 3 + private const val DIVIDER_POSITION = DIVIDER_INDEX - 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/FourDigitCardFormatWatcher.kt b/app/src/main/java/payselection/demo/utils/FourDigitCardFormatWatcher.kt new file mode 100644 index 0000000..96d06dd --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/FourDigitCardFormatWatcher.kt @@ -0,0 +1,62 @@ +package payselection.demo.utils + +import android.text.Editable +import android.text.TextWatcher + +class FourDigitCardFormatWatcher : TextWatcher { + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun afterTextChanged(s: Editable) { + if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_INDEX, DIVIDER)) { + s.replace(0, s.length, getDigitArray(s, TOTAL_DIGITS)?.let { buildCorrectString(it, DIVIDER_POSITION, DIVIDER) }); + } + } + + private fun isInputCorrect(s: Editable, totalSymbols: Int, dividerModulo: Int, divider: Char): Boolean { + var isCorrect = s.length <= totalSymbols // check size of entered string + for (i in 0 until s.length) { // check that every element is right + isCorrect = if (i > 0 && (i + 1) % dividerModulo == 0) { + isCorrect and (divider == s[i]) + } else { + isCorrect and Character.isDigit(s[i]) + } + } + return isCorrect + } + + private fun buildCorrectString(digits: CharArray, dividerPosition: Int, divider: Char): String? { + val formatted = StringBuilder() + for (i in digits.indices) { + if (digits[i].code != 0) { + formatted.append(digits[i]) + if (i > 0 && i < digits.size - 1 && (i + 1) % dividerPosition == 0) { + formatted.append(divider) + } + } + } + return formatted.toString() + } + + private fun getDigitArray(s: Editable, size: Int): CharArray? { + val digits = CharArray(size) + var index = 0 + var i = 0 + while (i < s.length && index < size) { + val current = s[i] + if (Character.isDigit(current)) { + digits[index] = current + index++ + } + i++ + } + return digits + } + + companion object { + private const val DIVIDER = ' ' + private const val TOTAL_SYMBOLS = 19 + private const val TOTAL_DIGITS = 16 + private const val DIVIDER_INDEX = 5 + private const val DIVIDER_POSITION = DIVIDER_INDEX - 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/ThreeDigitWatcher.kt b/app/src/main/java/payselection/demo/utils/ThreeDigitWatcher.kt new file mode 100644 index 0000000..0f0cbd1 --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/ThreeDigitWatcher.kt @@ -0,0 +1,29 @@ +package payselection.demo.utils + +import android.text.Editable +import android.text.TextWatcher + +class ThreeDigitWatcher : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable) { + if (!isInputCorrect(s, MAX_DIGIT)) { + val filteredText = s.subSequence(0, Math.min(s.length, MAX_DIGIT)).toString().filter { it.isDigit() } + s.replace(0, s.length, filteredText) + } + } + + private fun isInputCorrect(s: Editable, totalSymbols: Int): Boolean { + var isCorrect = s.length <= totalSymbols + for (element in s) { + isCorrect =isCorrect and Character.isDigit(element) + } + return isCorrect + } + + companion object { + private const val MAX_DIGIT = 3 + } +} \ No newline at end of file diff --git a/app/src/main/java/payselection/demo/utils/ViewUtil.kt b/app/src/main/java/payselection/demo/utils/ViewUtil.kt new file mode 100644 index 0000000..02977bd --- /dev/null +++ b/app/src/main/java/payselection/demo/utils/ViewUtil.kt @@ -0,0 +1,19 @@ +package payselection.demo.utils + +import android.content.Context +import android.content.res.ColorStateList +import androidx.core.content.ContextCompat +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import payselection.demo.R + +fun TextInputLayout.updateColor(context: Context, isError: Boolean, validText: String, errorText: String) { + hint = if (isError) errorText else validText + defaultHintTextColor = + if (isError) ColorStateList.valueOf(ContextCompat.getColor(context, R.color.error)) else ColorStateList.valueOf(ContextCompat.getColor(context, R.color.gray)) + if (isError) setBackgroundResource(R.drawable.bg_edittext_error) else setBackgroundResource(R.drawable.bg_edit_text) +} + +fun TextInputEditText.updateColor(context: Context, isError: Boolean) { + if (isError) setTextColor(ContextCompat.getColor(context, R.color.error)) else setTextColor(ContextCompat.getColor(context, R.color.black)) +} \ No newline at end of file diff --git "a/app/src/main/java/payselection/demo/utils/\320\241onstants.kt" "b/app/src/main/java/payselection/demo/utils/\320\241onstants.kt" new file mode 100644 index 0000000..88f5ecc --- /dev/null +++ "b/app/src/main/java/payselection/demo/utils/\320\241onstants.kt" @@ -0,0 +1,4 @@ +package payselection.demo.utils + +const val ADD_ITEM_INDEX = -1 +const val EMPTY_STRING = "" \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_add_card_select.xml b/app/src/main/res/drawable/bg_add_card_select.xml new file mode 100644 index 0000000..5d836bf --- /dev/null +++ b/app/src/main/res/drawable/bg_add_card_select.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button.xml b/app/src/main/res/drawable/bg_button.xml new file mode 100644 index 0000000..2fc53a6 --- /dev/null +++ b/app/src/main/res/drawable/bg_button.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_button_not_enabled.xml b/app/src/main/res/drawable/bg_button_not_enabled.xml new file mode 100644 index 0000000..93e2339 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_not_enabled.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card.xml b/app/src/main/res/drawable/bg_card.xml new file mode 100644 index 0000000..3d63f9f --- /dev/null +++ b/app/src/main/res/drawable/bg_card.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_add_selected.xml b/app/src/main/res/drawable/bg_card_add_selected.xml new file mode 100644 index 0000000..b76dda0 --- /dev/null +++ b/app/src/main/res/drawable/bg_card_add_selected.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_type.xml b/app/src/main/res/drawable/bg_card_type.xml new file mode 100644 index 0000000..ece5c43 --- /dev/null +++ b/app/src/main/res/drawable/bg_card_type.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_type_add.xml b/app/src/main/res/drawable/bg_card_type_add.xml new file mode 100644 index 0000000..dc40d2c --- /dev/null +++ b/app/src/main/res/drawable/bg_card_type_add.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edit_text.xml b/app/src/main/res/drawable/bg_edit_text.xml new file mode 100644 index 0000000..e86b531 --- /dev/null +++ b/app/src/main/res/drawable/bg_edit_text.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edittext_error.xml b/app/src/main/res/drawable/bg_edittext_error.xml new file mode 100644 index 0000000..b5525a0 --- /dev/null +++ b/app/src/main/res/drawable/bg_edittext_error.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_product_image.xml b/app/src/main/res/drawable/bg_product_image.xml new file mode 100644 index 0000000..8e0befb --- /dev/null +++ b/app/src/main/res/drawable/bg_product_image.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_select_card.xml b/app/src/main/res/drawable/bg_select_card.xml new file mode 100644 index 0000000..a551b07 --- /dev/null +++ b/app/src/main/res/drawable/bg_select_card.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_sheet_bottom.xml b/app/src/main/res/drawable/bg_sheet_bottom.xml new file mode 100644 index 0000000..ec60aab --- /dev/null +++ b/app/src/main/res/drawable/bg_sheet_bottom.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/i_ellipse.xml b/app/src/main/res/drawable/i_ellipse.xml new file mode 100644 index 0000000..7488da9 --- /dev/null +++ b/app/src/main/res/drawable/i_ellipse.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/i_ellipse_red.xml b/app/src/main/res/drawable/i_ellipse_red.xml new file mode 100644 index 0000000..f00233d --- /dev/null +++ b/app/src/main/res/drawable/i_ellipse_red.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_basket.xml b/app/src/main/res/drawable/ic_basket.xml new file mode 100644 index 0000000..5504160 --- /dev/null +++ b/app/src/main/res/drawable/ic_basket.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..28b3ae1 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_devider.xml b/app/src/main/res/drawable/ic_devider.xml new file mode 100644 index 0000000..f0a6848 --- /dev/null +++ b/app/src/main/res/drawable/ic_devider.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 0000000..631508c --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml new file mode 100644 index 0000000..152584d --- /dev/null +++ b/app/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eye_close.xml b/app/src/main/res/drawable/ic_eye_close.xml new file mode 100644 index 0000000..4a74d04 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_eye_close_with_line.xml b/app/src/main/res/drawable/ic_eye_close_with_line.xml new file mode 100644 index 0000000..92b0fe0 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_close_with_line.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye_with_line.xml b/app/src/main/res/drawable/ic_eye_with_line.xml new file mode 100644 index 0000000..bb45f5c --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_with_line.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 0000000..af838f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_line.xml b/app/src/main/res/drawable/ic_line.xml new file mode 100644 index 0000000..882d6e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_line.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_line_bottom_sheet.xml b/app/src/main/res/drawable/ic_line_bottom_sheet.xml new file mode 100644 index 0000000..f9e16f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_line_bottom_sheet.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mastercard.xml b/app/src/main/res/drawable/ic_mastercard.xml new file mode 100644 index 0000000..9f98699 --- /dev/null +++ b/app/src/main/res/drawable/ic_mastercard.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_mastercard_with_line.xml b/app/src/main/res/drawable/ic_mastercard_with_line.xml new file mode 100644 index 0000000..09f6c98 --- /dev/null +++ b/app/src/main/res/drawable/ic_mastercard_with_line.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mir.xml b/app/src/main/res/drawable/ic_mir.xml new file mode 100644 index 0000000..219b418 --- /dev/null +++ b/app/src/main/res/drawable/ic_mir.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mir_with_line.xml b/app/src/main/res/drawable/ic_mir_with_line.xml new file mode 100644 index 0000000..4d9208c --- /dev/null +++ b/app/src/main/res/drawable/ic_mir_with_line.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..9e37aed --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_ready.xml b/app/src/main/res/drawable/ic_ready.xml new file mode 100644 index 0000000..0882e55 --- /dev/null +++ b/app/src/main/res/drawable/ic_ready.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_ready_blue.xml b/app/src/main/res/drawable/ic_ready_blue.xml new file mode 100644 index 0000000..86d5749 --- /dev/null +++ b/app/src/main/res/drawable/ic_ready_blue.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_visa.xml b/app/src/main/res/drawable/ic_visa.xml new file mode 100644 index 0000000..04d01d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_visa.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_visa_with_line.xml b/app/src/main/res/drawable/ic_visa_with_line.xml new file mode 100644 index 0000000..cfe02c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_visa_with_line.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_card_1.png b/app/src/main/res/drawable/image_card_1.png new file mode 100644 index 0000000..ca69903 Binary files /dev/null and b/app/src/main/res/drawable/image_card_1.png differ diff --git a/app/src/main/res/drawable/image_card_2.png b/app/src/main/res/drawable/image_card_2.png new file mode 100644 index 0000000..52a93a8 Binary files /dev/null and b/app/src/main/res/drawable/image_card_2.png differ diff --git a/app/src/main/res/drawable/image_card_3.png b/app/src/main/res/drawable/image_card_3.png new file mode 100644 index 0000000..7931e6b Binary files /dev/null and b/app/src/main/res/drawable/image_card_3.png differ diff --git a/app/src/main/res/drawable/password_toggle.xml b/app/src/main/res/drawable/password_toggle.xml new file mode 100644 index 0000000..6d677bf --- /dev/null +++ b/app/src/main/res/drawable/password_toggle.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_button.xml b/app/src/main/res/drawable/selector_button.xml new file mode 100644 index 0000000..66b00c6 --- /dev/null +++ b/app/src/main/res/drawable/selector_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/raleway_100.ttf b/app/src/main/res/font/raleway_100.ttf new file mode 100644 index 0000000..9a8cc73 Binary files /dev/null and b/app/src/main/res/font/raleway_100.ttf differ diff --git a/app/src/main/res/font/raleway_100_italic.ttf b/app/src/main/res/font/raleway_100_italic.ttf new file mode 100644 index 0000000..49deb0b Binary files /dev/null and b/app/src/main/res/font/raleway_100_italic.ttf differ diff --git a/app/src/main/res/font/raleway_200.ttf b/app/src/main/res/font/raleway_200.ttf new file mode 100644 index 0000000..5b78f5e Binary files /dev/null and b/app/src/main/res/font/raleway_200.ttf differ diff --git a/app/src/main/res/font/raleway_200_italic.ttf b/app/src/main/res/font/raleway_200_italic.ttf new file mode 100644 index 0000000..95231ff Binary files /dev/null and b/app/src/main/res/font/raleway_200_italic.ttf differ diff --git a/app/src/main/res/font/raleway_300.ttf b/app/src/main/res/font/raleway_300.ttf new file mode 100644 index 0000000..50c2d6f Binary files /dev/null and b/app/src/main/res/font/raleway_300.ttf differ diff --git a/app/src/main/res/font/raleway_300_italic.ttf b/app/src/main/res/font/raleway_300_italic.ttf new file mode 100644 index 0000000..2c6dae7 Binary files /dev/null and b/app/src/main/res/font/raleway_300_italic.ttf differ diff --git a/app/src/main/res/font/raleway_400.ttf b/app/src/main/res/font/raleway_400.ttf new file mode 100644 index 0000000..9a70667 Binary files /dev/null and b/app/src/main/res/font/raleway_400.ttf differ diff --git a/app/src/main/res/font/raleway_400_italic.ttf b/app/src/main/res/font/raleway_400_italic.ttf new file mode 100644 index 0000000..fa7ca30 Binary files /dev/null and b/app/src/main/res/font/raleway_400_italic.ttf differ diff --git a/app/src/main/res/font/raleway_500.ttf b/app/src/main/res/font/raleway_500.ttf new file mode 100644 index 0000000..015d810 Binary files /dev/null and b/app/src/main/res/font/raleway_500.ttf differ diff --git a/app/src/main/res/font/raleway_500_italic.ttf b/app/src/main/res/font/raleway_500_italic.ttf new file mode 100644 index 0000000..2b8b88c Binary files /dev/null and b/app/src/main/res/font/raleway_500_italic.ttf differ diff --git a/app/src/main/res/font/raleway_600.ttf b/app/src/main/res/font/raleway_600.ttf new file mode 100644 index 0000000..85d41ed Binary files /dev/null and b/app/src/main/res/font/raleway_600.ttf differ diff --git a/app/src/main/res/font/raleway_600_italic.ttf b/app/src/main/res/font/raleway_600_italic.ttf new file mode 100644 index 0000000..da1dcc2 Binary files /dev/null and b/app/src/main/res/font/raleway_600_italic.ttf differ diff --git a/app/src/main/res/font/raleway_700.ttf b/app/src/main/res/font/raleway_700.ttf new file mode 100644 index 0000000..16db3eb Binary files /dev/null and b/app/src/main/res/font/raleway_700.ttf differ diff --git a/app/src/main/res/font/raleway_700_italic.ttf b/app/src/main/res/font/raleway_700_italic.ttf new file mode 100644 index 0000000..5a429a4 Binary files /dev/null and b/app/src/main/res/font/raleway_700_italic.ttf differ diff --git a/app/src/main/res/font/raleway_800.ttf b/app/src/main/res/font/raleway_800.ttf new file mode 100644 index 0000000..42dd80b Binary files /dev/null and b/app/src/main/res/font/raleway_800.ttf differ diff --git a/app/src/main/res/font/raleway_800_italic.ttf b/app/src/main/res/font/raleway_800_italic.ttf new file mode 100644 index 0000000..c309333 Binary files /dev/null and b/app/src/main/res/font/raleway_800_italic.ttf differ diff --git a/app/src/main/res/font/raleway_900.ttf b/app/src/main/res/font/raleway_900.ttf new file mode 100644 index 0000000..3d2bcc2 Binary files /dev/null and b/app/src/main/res/font/raleway_900.ttf differ diff --git a/app/src/main/res/font/raleway_900_italic.ttf b/app/src/main/res/font/raleway_900_italic.ttf new file mode 100644 index 0000000..76d31cd Binary files /dev/null and b/app/src/main/res/font/raleway_900_italic.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 44390cc..050c944 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,12 +4,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="payselection.demo.MainActivity"> + tools:context="payselection.demo.ui.MainActivity"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +