Skip to content

Commit

Permalink
Merge pull request #679 from Adyen/feature/full-address-form
Browse files Browse the repository at this point in the history
Full Address Form
  • Loading branch information
ozgur00 authored May 9, 2022
2 parents 3ee122d + 4c32396 commit 8f41e42
Show file tree
Hide file tree
Showing 75 changed files with 2,858 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package com.adyen.checkout.bcmc
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.adyen.checkout.card.CardValidationMapper
import com.adyen.checkout.card.CardValidationUtils
import com.adyen.checkout.card.util.CardValidationUtils
import com.adyen.checkout.card.api.model.Brand
import com.adyen.checkout.card.data.CardType
import com.adyen.checkout.card.data.ExpiryDate
Expand Down
71 changes: 71 additions & 0 deletions card/src/main/java/com/adyen/checkout/card/AddressConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.adyen.checkout.card

import android.os.Parcel
import android.os.Parcelable

/**
* Configuration class for Address Form in Card Component. This class can be used define the
* visibility of the address form.
*/
sealed class AddressConfiguration : Parcelable {

/**
* Address Form will be hidden.
*/
object None : AddressConfiguration() {
@JvmField
val CREATOR = object : Parcelable.Creator<None> {
override fun createFromParcel(source: Parcel?) = None
override fun newArray(size: Int) = arrayOfNulls<None>(size)
}
override fun describeContents() = Parcelable.CONTENTS_FILE_DESCRIPTOR
override fun writeToParcel(dest: Parcel?, flags: Int) {
// no ops
}
}

/**
* Only postal code will be shown as part of the card component.
*/
object PostalCode : AddressConfiguration() {
@JvmField
val CREATOR = object : Parcelable.Creator<PostalCode> {
override fun createFromParcel(source: Parcel?) = PostalCode
override fun newArray(size: Int) = arrayOfNulls<PostalCode>(size)
}
override fun describeContents() = Parcelable.CONTENTS_FILE_DESCRIPTOR
override fun writeToParcel(dest: Parcel?, flags: Int) {
// no ops
}
}

/**
* Full Address Form will be shown as part of the card component.
*
* @param defaultCountryCode Default country to be selected while initializing the form.
* @param supportedCountryCodes Supported country codes to be filtered from the available country
* options.
*/
data class FullAddress(
val defaultCountryCode: String? = null,
val supportedCountryCodes: List<String> = emptyList()
) : AddressConfiguration() {
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<FullAddress> {
@Suppress("UNCHECKED_CAST")
override fun createFromParcel(source: Parcel) = FullAddress(
defaultCountryCode = source.readString(),
supportedCountryCodes = source.readArrayList(String::class.java.classLoader) as List<String>
)
override fun newArray(size: Int) = arrayOfNulls<FullAddress>(size)
}
}

override fun describeContents() = Parcelable.CONTENTS_FILE_DESCRIPTOR
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(defaultCountryCode)
dest.writeList(supportedCountryCodes)
}
}
}
82 changes: 82 additions & 0 deletions card/src/main/java/com/adyen/checkout/card/AddressDelegate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2022 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ozgur on 18/3/2022.
*/

package com.adyen.checkout.card

import android.util.LruCache
import com.adyen.checkout.card.api.AddressDataType
import com.adyen.checkout.card.api.model.AddressItem
import com.adyen.checkout.card.repository.AddressRepository
import com.adyen.checkout.card.ui.AddressFormInput
import com.adyen.checkout.components.base.Configuration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

class AddressDelegate(
private val addressRepository: AddressRepository
) {

private val _statesFlow: MutableStateFlow<List<AddressItem>> = MutableStateFlow(emptyList())
internal val statesFlow: Flow<List<AddressItem>> = _statesFlow

private val cache: LruCache<String, List<AddressItem>> = LruCache<String, List<AddressItem>>(CACHE_ENTRY_SIZE)

fun getStateList(
configuration: Configuration,
countryCode: String?,
coroutineScope: CoroutineScope
) {
val addressSpecification = AddressFormInput.AddressSpecification.fromString(countryCode)
val needsStates = COUNTRIES_WITH_STATES.contains(addressSpecification)
if (!countryCode.isNullOrEmpty() && needsStates) {
cache[countryCode]?.let {
_statesFlow.tryEmit(it)
} ?: coroutineScope.launch {
val states = addressRepository.getAddressData(
environment = configuration.environment,
dataType = AddressDataType.STATE,
localeString = configuration.shopperLocale.toLanguageTag(),
countryCode = countryCode
)
if (states.isNotEmpty()) {
cache.put(countryCode, states)
}
_statesFlow.tryEmit(states)
}
} else {
_statesFlow.tryEmit(emptyList())
}
}

suspend fun getCountryList(configuration: Configuration): List<AddressItem> {
return cache[COUNTRIES_CACHE_KEY] ?: run {
val countries = addressRepository.getAddressData(
environment = configuration.environment,
dataType = AddressDataType.COUNTRY,
localeString = configuration.shopperLocale.toLanguageTag()
)
if (countries.isNotEmpty()) {
cache.put(COUNTRIES_CACHE_KEY, countries)
}
countries
}
}

companion object {
private val COUNTRIES_WITH_STATES = listOf(
AddressFormInput.AddressSpecification.BR,
AddressFormInput.AddressSpecification.CA,
AddressFormInput.AddressSpecification.US
)
// Only US, CA and BR has states and there's only one countries list.
private val CACHE_ENTRY_SIZE = COUNTRIES_WITH_STATES.size + 1
private const val COUNTRIES_CACHE_KEY = "countries"
}
}
24 changes: 24 additions & 0 deletions card/src/main/java/com/adyen/checkout/card/AddressFormUIState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.adyen.checkout.card

import com.adyen.checkout.components.base.AddressVisibility

enum class AddressFormUIState {
NONE, POSTAL_CODE, FULL_ADDRESS;

companion object {
fun fromAddressConfiguration(addressConfiguration: AddressConfiguration): AddressFormUIState {
return when (addressConfiguration) {
is AddressConfiguration.FullAddress -> FULL_ADDRESS
is AddressConfiguration.PostalCode -> POSTAL_CODE
is AddressConfiguration.None -> NONE
}
}

fun fromAddressVisibility(addressVisibility: AddressVisibility): AddressFormUIState {
return when (addressVisibility) {
AddressVisibility.POSTAL_CODE -> POSTAL_CODE
AddressVisibility.NONE -> NONE
}
}
}
}
34 changes: 34 additions & 0 deletions card/src/main/java/com/adyen/checkout/card/AddressInputModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ozgur on 8/3/2022.
*/

package com.adyen.checkout.card

data class AddressInputModel(
var postalCode: String = "",
var street: String = "",
var stateOrProvince: String = "",
var houseNumberOrName: String = "",
var apartmentSuite: String = "",
var city: String = "",
var country: String = "",
) {
/**
* Reset the data.
*
* Note: This method is called when country is changed and that's the reason [country] field
* does not get reset.
*/
fun reset() {
postalCode = ""
street = ""
stateOrProvince = ""
houseNumberOrName = ""
apartmentSuite = ""
city = ""
}
}
32 changes: 32 additions & 0 deletions card/src/main/java/com/adyen/checkout/card/AddressOutputData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by ozgur on 8/3/2022.
*/

package com.adyen.checkout.card

import com.adyen.checkout.components.base.OutputData
import com.adyen.checkout.components.ui.FieldState

data class AddressOutputData(
val postalCode: FieldState<String>,
val street: FieldState<String>,
val stateOrProvince: FieldState<String>,
val houseNumberOrName: FieldState<String>,
val apartmentSuite: FieldState<String>,
val city: FieldState<String>,
val country: FieldState<String>
) : OutputData {
override fun isValid(): Boolean {
return postalCode.validation.isValid() &&
street.validation.isValid() &&
stateOrProvince.validation.isValid() &&
houseNumberOrName.validation.isValid() &&
apartmentSuite.validation.isValid() &&
city.validation.isValid() &&
country.validation.isValid()
}
}
Loading

0 comments on commit 8f41e42

Please sign in to comment.