Skip to content

Commit

Permalink
Improve address lookup examples
Browse files Browse the repository at this point in the history
Support address lookup in card example with sessions.
Extract address lookup related code to AddressLookupRepository.

COAND-871
  • Loading branch information
ozgur00 committed Jun 13, 2024
1 parent 55f9e37 commit 64f6367
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 64f6367

Please sign in to comment.