Skip to content

Commit

Permalink
Merge pull request #1660 from Adyen/feature/address-lookup-example
Browse files Browse the repository at this point in the history
Improve address lookup examples
  • Loading branch information
ozgur00 authored Jun 13, 2024
2 parents 55f9e37 + 64f6367 commit 90b2048
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 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 3/6/2024.
*/

package com.adyen.checkout.example.repositories

import com.adyen.checkout.components.core.LookupAddress

sealed class AddressLookupCompletionState {
data class Error(val message: String = "Something went wrong") : AddressLookupCompletionState()
data class Address(val lookupAddress: LookupAddress) : AddressLookupCompletionState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,71 @@ import com.adyen.checkout.example.data.mock.model.MockAddressLookupResponse
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AddressLookupRepository @Inject constructor(
private val assetManager: AssetManager
assetManager: AssetManager
) {
private val mockAddressLookupOptions: List<LookupAddress>

fun getAddressLookupOptions(): List<LookupAddress> {
init {
val mockDataService = MockDataService(assetManager)
val json = mockDataService.readJsonFile("lookup_options.json")
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val adapter: JsonAdapter<MockAddressLookupResponse> = moshi.adapter(MockAddressLookupResponse::class.java)
return adapter.fromJson(json)?.options.orEmpty()
mockAddressLookupOptions = adapter.fromJson(json)?.options.orEmpty()
}

private val addressLookupQueryFlow = MutableStateFlow<String?>(null)

@OptIn(FlowPreview::class)
val addressLookupOptionsFlow: Flow<List<LookupAddress>> = addressLookupQueryFlow
.filterNotNull()
.debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION)
.map { query ->
queryAddressLookupOptions(query)
}

private val _addressLookupCompletionFlow: MutableStateFlow<AddressLookupCompletionState?> = MutableStateFlow(null)
val addressLookupCompletionFlow: Flow<AddressLookupCompletionState> = _addressLookupCompletionFlow
.asStateFlow()
.onEach { delay(ADDRESS_LOOKUP_COMPLETION_DELAY) }
.filterNotNull()

fun onQuery(query: String) {
addressLookupQueryFlow.tryEmit(query)
}

fun onAddressLookupCompleted(lookupAddress: LookupAddress) {
if (lookupAddress.id == ADDRESS_LOOKUP_ERROR_ITEM_ID) {
_addressLookupCompletionFlow.tryEmit(AddressLookupCompletionState.Error())
} else {
_addressLookupCompletionFlow.tryEmit(AddressLookupCompletionState.Address(lookupAddress))
}
}

private fun queryAddressLookupOptions(query: String): List<LookupAddress> {
return if (query == "empty") {
emptyList()
} else {
mockAddressLookupOptions
}
}

companion object {
private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L
private const val ADDRESS_LOOKUP_COMPLETION_DELAY = 400L
private const val ADDRESS_LOOKUP_ERROR_ITEM_ID = "error"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@ import com.adyen.checkout.dropin.RecurringDropInServiceResult
import com.adyen.checkout.example.data.storage.KeyValueStorage
import com.adyen.checkout.example.extensions.getLogTag
import com.adyen.checkout.example.extensions.toStringPretty
import com.adyen.checkout.example.repositories.AddressLookupCompletionState
import com.adyen.checkout.example.repositories.AddressLookupRepository
import com.adyen.checkout.example.repositories.PaymentsRepository
import com.adyen.checkout.redirect.RedirectComponent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
Expand All @@ -57,7 +54,6 @@ import javax.inject.Inject
* [onOrderRequest] and [onOrderCancel] and it handles the stored payment method removal flow by
* implementing [onRemoveStoredPaymentMethod].
*/
@OptIn(FlowPreview::class)
@Suppress("TooManyFunctions")
@AndroidEntryPoint
class ExampleAdvancedDropInService : DropInService() {
Expand All @@ -71,19 +67,26 @@ class ExampleAdvancedDropInService : DropInService() {
@Inject
lateinit var addressLookupRepository: AddressLookupRepository

private val addressLookupQueryFlow = MutableStateFlow<String?>(null)
override fun onCreate() {
super.onCreate()
addressLookupRepository.addressLookupOptionsFlow
.onEach { options ->
sendAddressLookupResult(AddressLookupDropInServiceResult.LookupResult(options))
}.launchIn(this)

init {
addressLookupQueryFlow
.debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION)
.filterNotNull()
.onEach { query ->
val options = if (query == "empty") {
emptyList()
} else {
addressLookupRepository.getAddressLookupOptions()
addressLookupRepository.addressLookupCompletionFlow
.onEach {
val result = when (it) {
is AddressLookupCompletionState.Address -> {
AddressLookupDropInServiceResult.LookupComplete(it.lookupAddress)
}
is AddressLookupCompletionState.Error -> AddressLookupDropInServiceResult.Error(
errorDialog = ErrorDialog(
message = it.message,
),
)
}
sendAddressLookupResult(AddressLookupDropInServiceResult.LookupResult(options))
sendAddressLookupResult(result)
}.launchIn(this)
}

Expand Down Expand Up @@ -403,18 +406,17 @@ class ExampleAdvancedDropInService : DropInService() {

override fun onAddressLookupQueryChanged(query: String) {
Log.d(TAG, "On address lookup query: $query")
addressLookupQueryFlow.tryEmit(query)
addressLookupRepository.onQuery(query)
}

override fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean {
Log.d(TAG, "On address lookup query completion: $lookupAddress")
return false
addressLookupRepository.onAddressLookupCompleted(lookupAddress)
return true
}

companion object {
private val TAG = getLogTag()
private const val RESULT_REFUSED = "refused"

private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class CardActivity : AppCompatActivity(), AddressLookupCallback {

override fun onLookupCompletion(lookupAddress: LookupAddress): Boolean {
Log.d(TAG, "on lookup completed $lookupAddress")
cardViewModel.onAddressLookupCompleted(lookupAddress)
cardViewModel.onAddressLookupCompletion(lookupAddress)
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@ import com.adyen.checkout.components.core.LookupAddress
import com.adyen.checkout.components.core.PaymentComponentData
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.example.data.storage.KeyValueStorage
import com.adyen.checkout.example.repositories.AddressLookupCompletionState
import com.adyen.checkout.example.repositories.AddressLookupRepository
import com.adyen.checkout.example.repositories.PaymentsRepository
import com.adyen.checkout.example.service.createPaymentRequest
import com.adyen.checkout.example.service.getPaymentMethodRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand All @@ -32,7 +30,6 @@ import kotlinx.coroutines.withContext
import org.json.JSONObject
import javax.inject.Inject

@OptIn(FlowPreview::class)
@Suppress("TooManyFunctions")
@HiltViewModel
internal class CardViewModel @Inject constructor(
Expand All @@ -48,25 +45,19 @@ internal class CardViewModel @Inject constructor(
private val _cardViewState = MutableStateFlow<CardViewState>(CardViewState.Loading)
val cardViewState: Flow<CardViewState> = _cardViewState

private val addressLookupQueryFlow = MutableStateFlow<String?>(null)

private val _events = MutableSharedFlow<CardEvent>()
val events: Flow<CardEvent> = _events

init {
viewModelScope.launch { fetchPaymentMethods() }
addressLookupQueryFlow
.filterNotNull()
.debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION)
.onEach { query ->
val options = if (query == "empty") {
emptyList()
} else {
addressLookupRepository.getAddressLookupOptions()
}
addressLookupRepository.addressLookupOptionsFlow
.onEach { options ->
_events.emit(CardEvent.AddressLookup(options))
}
.launchIn(viewModelScope)
}.launchIn(viewModelScope)
addressLookupRepository.addressLookupCompletionFlow
.onEach {
onAddressCompleted(it)
}.launchIn(viewModelScope)
}

private suspend fun fetchPaymentMethods() = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -111,20 +102,25 @@ internal class CardViewModel @Inject constructor(
}

fun onAddressLookupQueryChanged(query: String) {
viewModelScope.launch {
addressLookupQueryFlow.emit(query)
}
addressLookupRepository.onQuery(query)
}

fun onAddressLookupCompletion(lookupAddress: LookupAddress) {
addressLookupRepository.onAddressLookupCompleted(lookupAddress)
}

fun onAddressLookupCompleted(lookupAddress: LookupAddress) {
private fun onAddressCompleted(addressLookupCompletionState: AddressLookupCompletionState) {
viewModelScope.launch {
delay(ADDRESS_LOOKUP_COMPLETION_DELAY)
if (lookupAddress.id == ADDRESS_LOOKUP_ERROR_ITEM_ID) {
_events.emit(CardEvent.AddressLookupError("Something went wrong."))
} else {
_events.emit(
when (addressLookupCompletionState) {
is AddressLookupCompletionState.Address -> _events.emit(
CardEvent.AddressLookupCompleted(
addressLookupRepository.getAddressLookupOptions().first { it.id == lookupAddress.id },
addressLookupCompletionState.lookupAddress,
),
)

is AddressLookupCompletionState.Error -> _events.emit(
CardEvent.AddressLookupError(
addressLookupCompletionState.message,
),
)
}
Expand Down Expand Up @@ -183,10 +179,4 @@ internal class CardViewModel @Inject constructor(
private fun onComponentError(error: ComponentError) {
viewModelScope.launch { _events.emit(CardEvent.PaymentResult("Failed: ${error.errorMessage}")) }
}

companion object {
private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L
private const val ADDRESS_LOOKUP_COMPLETION_DELAY = 400L
private const val ADDRESS_LOOKUP_ERROR_ITEM_ID = "error"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.adyen.checkout.card.CardComponent
import com.adyen.checkout.components.core.AddressLookupCallback
import com.adyen.checkout.components.core.AddressLookupResult
import com.adyen.checkout.components.core.LookupAddress
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.example.databinding.ActivityCardBinding
Expand All @@ -30,6 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

@Suppress("TooManyFunctions")
@AndroidEntryPoint
class SessionsCardTakenOverActivity : AppCompatActivity(), AddressLookupCallback {

Expand Down Expand Up @@ -123,8 +125,8 @@ class SessionsCardTakenOverActivity : AppCompatActivity(), AddressLookupCallback
is CardEvent.PaymentResult -> onPaymentResult(event.result)
is CardEvent.AdditionalAction -> onAction(event.action)
is CardEvent.AddressLookup -> onAddressLookup(event.options)
is CardEvent.AddressLookupCompleted -> {}
is CardEvent.AddressLookupError -> {}
is CardEvent.AddressLookupCompleted -> onAddressLookupCompleted(event.lookupAddress)
is CardEvent.AddressLookupError -> onAddressLookupError(event.message)
}
}

Expand All @@ -146,6 +148,20 @@ class SessionsCardTakenOverActivity : AppCompatActivity(), AddressLookupCallback
cardViewModel.onAddressLookupQueryChanged(query)
}

override fun onLookupCompletion(lookupAddress: LookupAddress): Boolean {
Log.d(TAG, "On address lookup completion: $lookupAddress")
cardViewModel.onAddressLookupCompletion(lookupAddress)
return true
}

private fun onAddressLookupCompleted(lookupAddress: LookupAddress) {
cardComponent?.setAddressLookupResult(AddressLookupResult.Completed(lookupAddress))
}

private fun onAddressLookupError(message: String) {
cardComponent?.setAddressLookupResult(AddressLookupResult.Error(message))
}

override fun onDestroy() {
super.onDestroy()
cardComponent = null
Expand Down
Loading

0 comments on commit 90b2048

Please sign in to comment.