Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.stripe.android.checkout

import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Parcelable
import androidx.annotation.ColorInt
import androidx.annotation.FontRes
Expand All @@ -21,7 +20,6 @@ import com.stripe.android.paymentelement.CheckoutSessionPreview
import com.stripe.android.paymentsheet.analytics.PaymentSheetEvent
import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponse
import com.stripe.android.paymentsheet.verticalmode.CurrencySelectorToggle
import com.stripe.android.uicore.image.DefaultStripeImageLoader
import com.stripe.android.uicore.strings.resolve
import com.stripe.android.uicore.utils.collectAsState
import dev.drewhamilton.poko.Poko
Expand Down Expand Up @@ -78,7 +76,7 @@ class Checkout private constructor(
sessionId = checkoutSessionClientSecret.substringBefore("_secret_"),
adaptivePricingAllowed = configurationState.adaptivePricingAllowed,
).map { response ->
val flagImages = prefetchFlagImages(context, response, component)
val flagImages = component.flagImageResolver.resolve(response, cached = null)
val key = UUID.randomUUID().toString()
CheckoutInstances.getOrCreate(key) {
Checkout(
Expand All @@ -94,36 +92,6 @@ class Checkout private constructor(
}
}

private suspend fun prefetchFlagImages(
context: Context,
response: CheckoutSessionResponse,
component: CheckoutComponent,
): Map<String, Bitmap>? {
val adaptivePricingInfo = response.adaptivePricingInfo ?: return null
val localOption = adaptivePricingInfo.localCurrencyOptions.firstOrNull() ?: return null
val flagImageRepository = FlagImageRepository(
imageLoader = DefaultStripeImageLoader(context),
displayDensity = context.resources.displayMetrics.density,
)
val result = flagImageRepository.fetch(
integrationCurrencyCode = adaptivePricingInfo.integrationCurrency,
localCurrencyCode = localOption.currency,
)
for (failure in result.failures) {
val event = PaymentSheetEvent.AdaptivePricingFlagImageLoadFailed(
countryCode = failure.countryCode,
url = failure.url,
)
component.analyticsRequestExecutor.executeAsync(
component.paymentAnalyticsRequestFactory.createRequest(
event = event,
additionalParams = event.params,
)
)
}
return result.images
}

/**
* Returns the existing [Checkout] if one is still alive, or creates a new one from [state].
*/
Expand Down Expand Up @@ -606,8 +574,13 @@ class Checkout private constructor(
val result = runCatching {
internalState.block(internalState.checkoutSessionResponse.id).getOrThrow()
}.map { response ->
internalState =
internalState.copy(checkoutSessionResponse = response).additionalStateMutations()
val flagImages = component.flagImageResolver.resolve(
response = response,
cached = internalState.flagImages,
)
internalState = internalState
.copy(checkoutSessionResponse = response, flagImages = flagImages)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically new behavior. It should never really be triggered. But I want this in the new checkout controller. And it was easier to introduce it here than in one massive PR for checkout controller.

.additionalStateMutations()
_checkoutSession.value = internalState.asCheckoutSession()
}
result
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.stripe.android.checkout

import android.graphics.Bitmap
import com.stripe.android.common.di.DisplayDensity
import com.stripe.android.uicore.image.StripeImageLoader
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import javax.inject.Inject
import kotlin.math.roundToInt

internal class FlagImageRepository(
internal class FlagImageRepository @Inject constructor(
private val imageLoader: StripeImageLoader,
private val displayDensity: Float,
@DisplayDensity private val displayDensity: Float,
) {
suspend fun fetch(
integrationCurrencyCode: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.stripe.android.checkout

import android.graphics.Bitmap
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.networking.PaymentAnalyticsRequestFactory
import com.stripe.android.paymentsheet.analytics.PaymentSheetEvent
import com.stripe.android.paymentsheet.repositories.CheckoutSessionResponse
import javax.inject.Inject

/**
* Resolves the adaptive-pricing flag images for a [CheckoutSessionResponse].
*
* Flags are keyed by uppercase currency code, so when the response's integration and local
* currencies are both already present in the [cached] map (i.e. the currencies haven't changed
* across a mutation such as applying a promotion code), the cached images are reused instead of
* being re-downloaded. A failed fetch reports [PaymentSheetEvent.AdaptivePricingFlagImageLoadFailed]
* for each missing flag and leaves the images null so a later mutation can retry.
*/
internal class FlagImageResolver @Inject constructor(
private val flagImageRepository: FlagImageRepository,
private val analyticsRequestExecutor: AnalyticsRequestExecutor,
private val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory,
) {
suspend fun resolve(
response: CheckoutSessionResponse,
cached: Map<String, Bitmap>?,
): Map<String, Bitmap>? {
val adaptivePricingInfo = response.adaptivePricingInfo ?: return null
val localOption = adaptivePricingInfo.localCurrencyOptions.firstOrNull() ?: return null
val integrationCurrency = adaptivePricingInfo.integrationCurrency
val localCurrency = localOption.currency

if (cached != null &&
cached.containsKey(integrationCurrency.uppercase()) &&
cached.containsKey(localCurrency.uppercase())
) {
return cached
}

val result = flagImageRepository.fetch(
integrationCurrencyCode = integrationCurrency,
localCurrencyCode = localCurrency,
)
for (failure in result.failures) {
val event = PaymentSheetEvent.AdaptivePricingFlagImageLoadFailed(
countryCode = failure.countryCode,
url = failure.url,
)
analyticsRequestExecutor.executeAsync(
paymentAnalyticsRequestFactory.createRequest(
event = event,
additionalParams = event.params,
)
)
}
return result.images
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal data class InternalState(
val key: String,
val configuration: Checkout.Configuration.State,
val checkoutSessionResponse: CheckoutSessionResponse,
private val flagImages: Map<String, Bitmap>?,
val flagImages: Map<String, Bitmap>?,
val shippingName: String? = null,
val billingName: String? = null,
val shippingPhoneNumber: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.stripe.android.checkout.injection
import android.app.Application
import android.content.Context
import com.stripe.android.PaymentConfiguration
import com.stripe.android.checkout.FlagImageResolver
import com.stripe.android.common.di.DisplayDensity
import com.stripe.android.common.di.ElementsSessionClientParamsModule
import com.stripe.android.core.injection.CoreCommonModule
import com.stripe.android.core.injection.CoroutineContextModule
Expand All @@ -17,6 +19,8 @@ import com.stripe.android.payments.core.injection.PaymentConfigurationModule
import com.stripe.android.payments.core.injection.StripeRepositoryModule
import com.stripe.android.paymentsheet.BuildConfig
import com.stripe.android.paymentsheet.repositories.CheckoutSessionRepository
import com.stripe.android.uicore.image.DefaultStripeImageLoader
import com.stripe.android.uicore.image.StripeImageLoader
import dagger.BindsInstance
import dagger.Component
import dagger.Module
Expand All @@ -38,6 +42,7 @@ internal interface CheckoutComponent {
val checkoutSessionRepository: CheckoutSessionRepository
val analyticsRequestExecutor: AnalyticsRequestExecutor
val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory
val flagImageResolver: FlagImageResolver

@Component.Factory
interface Factory {
Expand Down Expand Up @@ -67,4 +72,11 @@ internal object CheckoutModule {
apiKey = paymentConfiguration.get().publishableKey,
stripeAccount = paymentConfiguration.get().stripeAccountId,
)

@Provides
fun provideStripeImageLoader(context: Context): StripeImageLoader = DefaultStripeImageLoader(context)

@Provides
@DisplayDensity
fun provideDisplayDensity(context: Context): Float = context.resources.displayMetrics.density
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stripe.android.common.di

import javax.inject.Qualifier

@Qualifier
internal annotation class DisplayDensity
Loading
Loading