diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt index 647b17ce340..a600d2db662 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/BlazeRepository.kt @@ -1,12 +1,94 @@ package com.woocommerce.android.ui.blaze import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.products.ProductDetailRepository +import com.woocommerce.android.util.TimezoneProvider import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore +import java.util.Date import javax.inject.Inject class BlazeRepository @Inject constructor( private val selectedSite: SelectedSite, - private val blazeCampaignsStore: BlazeCampaignsStore + private val blazeCampaignsStore: BlazeCampaignsStore, + private val productDetailRepository: ProductDetailRepository, + private val timezoneProvider: TimezoneProvider, ) { + companion object { + const val DEFAULT_CURRENCY_CODE = "USD" // For now only USD are supported + const val DEFAULT_CAMPAIGN_DURATION = 7 // Days + const val DEFAULT_CAMPAIGN_TOTAL_BUDGET = 35F // USD + const val ONE_DAY_IN_MILLIS = 1000 * 60 * 60 * 24 + } + suspend fun getMostRecentCampaign() = blazeCampaignsStore.getMostRecentBlazeCampaign(selectedSite.get()) + + fun getCampaignPreviewDetails(productId: Long): CampaignPreview { + val product = productDetailRepository.getProduct(productId) + return CampaignPreview( + productId = productId, + aiSuggestions = listOf(), + budget = Budget( + totalBudget = DEFAULT_CAMPAIGN_TOTAL_BUDGET, + spentBudget = 0f, + currencyCode = DEFAULT_CURRENCY_CODE, + durationInDays = DEFAULT_CAMPAIGN_DURATION, + startDate = Date().apply { time += ONE_DAY_IN_MILLIS }, // By default start tomorrow + ), + languages = listOf(), + devices = listOf(), + locations = listOf(), + interests = listOf(), + userTimeZone = timezoneProvider.deviceTimezone.displayName, + targetUrl = product?.permalink ?: "", + urlParams = null, + campaignImageUrl = product?.firstImageUrl + ) + } + + data class CampaignPreview( + val productId: Long, + val aiSuggestions: List, + val budget: Budget, + val languages: List, + val devices: List, + val locations: List, + val interests: List, + val userTimeZone: String, + val targetUrl: String, + val urlParams: Pair?, + val campaignImageUrl: String?, + ) + + data class AiSuggestionForAd( + val title: String, + val tagLine: String, + ) + + data class Budget( + val totalBudget: Float, + val spentBudget: Float, + val currencyCode: String, + val durationInDays: Int, + val startDate: Date, + ) + + data class Location( + val id: String, + val name: String, + ) + + data class Language( + val code: String, + val name: String, + ) + + data class Device( + val id: String, + val name: String, + ) + + data class Interest( + val id: String, + val description: String, + ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewFragment.kt index 41caf619b26..a3501a325c9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewFragment.kt @@ -5,10 +5,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import com.woocommerce.android.ui.base.BaseFragment import com.woocommerce.android.ui.compose.composeView import com.woocommerce.android.ui.main.AppBarStatus +import com.woocommerce.android.viewmodel.MultiLiveEvent import dagger.hilt.android.AndroidEntryPoint +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel @AndroidEntryPoint class BlazeCampaignCreationPreviewFragment : BaseFragment() { @@ -22,4 +25,17 @@ class BlazeCampaignCreationPreviewFragment : BaseFragment() { BlazeCampaignCreationPreviewScreen(viewModel = viewModel) } } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupObservers() + } + + private fun setupObservers() { + viewModel.event.observe(viewLifecycleOwner) { event -> + when (event) { + is MultiLiveEvent.Event.Exit -> findNavController().popBackStack() + } + } + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt index a675e2ba818..9aaab43bf22 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewScreen.kt @@ -41,31 +41,37 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import com.woocommerce.android.R -import com.woocommerce.android.R.string -import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState -import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState.CampaignDetailItem -import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState.CampaignPreviewContent -import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState.Loading import com.woocommerce.android.ui.compose.animations.SkeletonView import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCColoredButton import com.woocommerce.android.ui.compose.component.WCTextButton import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.AdDetailsUi +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignDetailItemUi +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignDetailsUi +import ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState @Composable fun BlazeCampaignCreationPreviewScreen(viewModel: BlazeCampaignCreationPreviewViewModel) { viewModel.viewState.observeAsState().value?.let { previewState -> - BlazeCampaignCreationPreviewScreen(previewState) + BlazeCampaignCreationPreviewScreen( + previewState, + viewModel::onBackPressed + ) } } @Composable -private fun BlazeCampaignCreationPreviewScreen(previewState: CampaignPreviewUiState) { +private fun BlazeCampaignCreationPreviewScreen( + previewState: CampaignPreviewUiState, + onBackPressed: () -> Unit +) { Scaffold( topBar = { Toolbar( title = stringResource(id = R.string.blaze_campaign_screen_fragment_title), - onNavigationButtonClick = { /*TODO*/ }, + onNavigationButtonClick = onBackPressed, navigationIcon = Filled.ArrowBack ) }, @@ -78,11 +84,18 @@ private fun BlazeCampaignCreationPreviewScreen(previewState: CampaignPreviewUiSt .padding(paddingValues) .background(color = MaterialTheme.colors.surface) ) { - when (previewState) { - is Loading -> CampaignPreviewLoading() - is CampaignPreviewContent -> CampaignPreviewContent(state = previewState) + + when { + previewState.isLoading -> AdDetailsLoading() + else -> AdDetailsHeader(state = previewState) } + CampaignDetails( + campaignDetails = previewState.campaignDetails, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) Spacer(modifier = Modifier.height(16.dp)) Divider() WCColoredButton( @@ -92,14 +105,14 @@ private fun BlazeCampaignCreationPreviewScreen(previewState: CampaignPreviewUiSt .padding(bottom = 8.dp), text = stringResource(id = R.string.blaze_campaign_preview_details_confirm_details_button), onClick = { /*TODO*/ }, - enabled = previewState !is Loading + enabled = !previewState.isLoading ) } } } @Composable -private fun CampaignPreviewLoading( +private fun AdDetailsLoading( modifier: Modifier = Modifier ) { Column( @@ -158,30 +171,22 @@ private fun CampaignPreviewLoading( } @Composable -fun CampaignPreviewContent( - state: CampaignPreviewContent, +fun AdDetailsHeader( + state: CampaignPreviewUiState, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxWidth()) { - CampaignHeader( - state = state, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color = colorResource(id = R.color.blaze_campaign_preview_header_background)) - ) - CampaignDetails( - state = state, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) - } + CampaignHeader( + adDetails = state.adDetails, + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color = colorResource(id = R.color.blaze_campaign_preview_header_background)) + ) } @Composable -fun CampaignHeader(state: CampaignPreviewContent, modifier: Modifier = Modifier) { +fun CampaignHeader(adDetails: AdDetailsUi, modifier: Modifier = Modifier) { Column( modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -202,7 +207,7 @@ fun CampaignHeader(state: CampaignPreviewContent, modifier: Modifier = Modifier) ) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(state.campaignImageUrl) + .data(adDetails.campaignImageUrl) .crossfade(true) .build(), fallback = painterResource(R.drawable.blaze_campaign_product_placeholder), @@ -217,7 +222,7 @@ fun CampaignHeader(state: CampaignPreviewContent, modifier: Modifier = Modifier) ) Text( modifier = Modifier.padding(top = 12.dp), - text = state.tagLine, + text = adDetails.tagLine, style = MaterialTheme.typography.caption, ) Row( @@ -228,7 +233,7 @@ fun CampaignHeader(state: CampaignPreviewContent, modifier: Modifier = Modifier) ) { Text( modifier = Modifier.weight(1f), - text = state.title, + text = adDetails.title, style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, ) @@ -256,7 +261,7 @@ fun CampaignHeader(state: CampaignPreviewContent, modifier: Modifier = Modifier) @Composable fun CampaignDetails( - state: CampaignPreviewContent, + campaignDetails: CampaignDetailsUi, modifier: Modifier = Modifier ) { Column(modifier = modifier) { @@ -266,21 +271,21 @@ fun CampaignDetails( style = MaterialTheme.typography.body2 ) // Budget - CampaignPropertyGroupItem(items = listOf(state.budget)) + CampaignPropertyGroupItem(items = listOf(campaignDetails.budget)) Spacer(modifier = Modifier.height(16.dp)) // Ad Audience - CampaignPropertyGroupItem(items = state.audienceDetails) + CampaignPropertyGroupItem(items = campaignDetails.targetDetails) Spacer(modifier = Modifier.height(16.dp)) // Destination - CampaignPropertyGroupItem(items = listOf(state.destinationUrl)) + CampaignPropertyGroupItem(items = listOf(campaignDetails.destinationUrl)) } } @Composable private fun CampaignPropertyGroupItem( - items: List, + items: List, modifier: Modifier = Modifier ) { val borderWidth = 1.dp @@ -304,7 +309,7 @@ private fun CampaignPropertyGroupItem( @Composable private fun CampaignPropertyItem( - item: CampaignDetailItem, + item: CampaignDetailItemUi, modifier: Modifier = Modifier, ) { Row( @@ -348,44 +353,50 @@ private fun CampaignPropertyItem( @Composable fun CampaignScreenPreview() { BlazeCampaignCreationPreviewScreen( - CampaignPreviewContent( - productId = 123, - title = "Get the latest white t-shirts", - tagLine = "From 45.00 USD", - campaignImageUrl = "https://rb.gy/gmjuwb", - budget = CampaignDetailItem( - displayTitle = stringResource(R.string.blaze_campaign_preview_details_budget), - displayValue = "140 USD, 7 days from Jan 14", + CampaignPreviewUiState( + isLoading = false, + adDetails = AdDetailsUi( + productId = 123, + title = "Get the latest white t-shirts", + tagLine = "From 45.00 USD", + campaignImageUrl = "https://rb.gy/gmjuwb", ), - audienceDetails = listOf( - CampaignDetailItem( - displayTitle = stringResource(string.blaze_campaign_preview_details_language), - displayValue = "English, Spanish", - ), - CampaignDetailItem( - displayTitle = stringResource(string.blaze_campaign_preview_details_devices), - displayValue = "USA, Poland, Japan", - ), - CampaignDetailItem( - displayTitle = stringResource(string.blaze_campaign_preview_details_location), - displayValue = "Samsung, Apple, Xiaomi", + campaignDetails = CampaignDetailsUi( + budget = CampaignDetailItemUi( + displayTitle = stringResource(R.string.blaze_campaign_preview_details_budget), + displayValue = "140 USD, 7 days from Jan 14", ), - CampaignDetailItem( - displayTitle = stringResource(string.blaze_campaign_preview_details_interests), - displayValue = "Fashion, Clothing, T-shirts", + targetDetails = listOf( + CampaignDetailItemUi( + displayTitle = stringResource(R.string.blaze_campaign_preview_details_language), + displayValue = "English, Spanish", + ), + CampaignDetailItemUi( + displayTitle = stringResource(R.string.blaze_campaign_preview_details_devices), + displayValue = "USA, Poland, Japan", + ), + CampaignDetailItemUi( + displayTitle = stringResource(R.string.blaze_campaign_preview_details_location), + displayValue = "Samsung, Apple, Xiaomi", + ), + CampaignDetailItemUi( + displayTitle = stringResource(R.string.blaze_campaign_preview_details_interests), + displayValue = "Fashion, Clothing, T-shirts", + ), ), - ), - destinationUrl = CampaignDetailItem( - displayTitle = "Destination URL", - displayValue = "https://www.myer.com.au/p/white-t-shirt-797334760-797334760", - maxLinesValue = 1, + destinationUrl = CampaignDetailItemUi( + displayTitle = "Destination URL", + displayValue = "https://www.myer.com.au/p/white-t-shirt-797334760-797334760", + maxLinesValue = 1, + ) ) - ) + ), + onBackPressed = { } ) } @LightDarkThemePreviews @Composable -fun CampaignLoadingScreenPreview() { - CampaignPreviewLoading() +fun AdDetailsLoadingPreview() { + AdDetailsLoading() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt index ac0567db29f..196c7c41e15 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/blaze/creation/preview/BlazeCampaignCreationPreviewViewModel.kt @@ -1,10 +1,14 @@ -package com.woocommerce.android.ui.blaze.creation.preview +package ui.blaze.creation.preview import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R -import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewViewModel.CampaignPreviewUiState.CampaignDetailItem -import com.woocommerce.android.ui.products.ProductDetailRepository +import com.woocommerce.android.extensions.formatToMMMdd +import com.woocommerce.android.ui.blaze.BlazeRepository +import com.woocommerce.android.ui.blaze.BlazeRepository.CampaignPreview +import com.woocommerce.android.ui.blaze.creation.preview.BlazeCampaignCreationPreviewFragmentArgs +import com.woocommerce.android.util.CurrencyFormatter +import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel import com.woocommerce.android.viewmodel.navArgs @@ -16,73 +20,113 @@ import javax.inject.Inject @HiltViewModel class BlazeCampaignCreationPreviewViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - productDetailRepository: ProductDetailRepository, - resourceProvider: ResourceProvider + blazeRepository: BlazeRepository, + private val resourceProvider: ResourceProvider, + private val currencyFormatter: CurrencyFormatter ) : ScopedViewModel(savedStateHandle) { + private val navArgs: BlazeCampaignCreationPreviewFragmentArgs by savedStateHandle.navArgs() - private val _viewState = MutableLiveData(CampaignPreviewUiState.Loading) + private val _viewState = MutableLiveData( + blazeRepository + .getCampaignPreviewDetails(navArgs.productId) + .toCampaignPreviewUiState(isLoading = true) + ) val viewState = _viewState - private val navArgs: BlazeCampaignCreationPreviewFragmentArgs by savedStateHandle.navArgs() - init { launch { @Suppress("MagicNumber") - delay(5000) - val product = productDetailRepository.getProduct(navArgs.productId) - _viewState.value = CampaignPreviewUiState.CampaignPreviewContent( - productId = product?.remoteId ?: -1, - title = "Get the latest white t-shirts", - tagLine = "From 45.00 USD", - campaignImageUrl = "https://rb.gy/gmjuwb", - budget = CampaignDetailItem( - displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_budget), - displayValue = "140 USD, 7 days from Jan 14", - ), - audienceDetails = listOf( - CampaignDetailItem( - displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_language), - displayValue = "English, Spanish", - ), - CampaignDetailItem( - displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_devices), - displayValue = "USA, Poland, Japan", - ), - CampaignDetailItem( - displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_location), - displayValue = "Samsung, Apple, Xiaomi", - ), - CampaignDetailItem( - displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_interests), - displayValue = "Fashion, Clothing, T-shirts", - ), - ), - destinationUrl = CampaignDetailItem( - displayTitle = "Destination URL", - displayValue = "https://www.myer.com.au/p/white-t-shirt-797334760-797334760", - maxLinesValue = 1, - ) - ) + delay(3000) + _viewState.value = _viewState.value?.copy(isLoading = false) } } - sealed interface CampaignPreviewUiState { - object Loading : CampaignPreviewUiState - data class CampaignPreviewContent( - val isLoading: Boolean = false, - val productId: Long, - val title: String, - val tagLine: String, - val campaignImageUrl: String, - val budget: CampaignDetailItem, - val audienceDetails: List, - val destinationUrl: CampaignDetailItem, - ) : CampaignPreviewUiState + fun onBackPressed() { + triggerEvent(MultiLiveEvent.Event.Exit) + } - data class CampaignDetailItem( - val displayTitle: String, - val displayValue: String, - val maxLinesValue: Int? = null, + private fun CampaignPreview.toCampaignPreviewUiState(isLoading: Boolean = false) = + CampaignPreviewUiState( + isLoading = isLoading, + adDetails = AdDetailsUi( + productId = productId, + title = aiSuggestions.firstOrNull()?.title ?: "", + tagLine = aiSuggestions.firstOrNull()?.tagLine ?: "", + campaignImageUrl = campaignImageUrl ?: "", + ), + campaignDetails = toCampaignDetailsUi() ) + + private fun CampaignPreview.toCampaignDetailsUi() = + CampaignDetailsUi( + budget = CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_budget), + displayValue = budget.toDisplayValue(), + ), + targetDetails = listOf( + CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_language), + displayValue = languages.joinToString { it.name } + .ifEmpty { resourceProvider.getString(R.string.blaze_campaign_preview_target_default_value) }, + ), + CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_devices), + displayValue = locations.joinToString { it.name } + .ifEmpty { resourceProvider.getString(R.string.blaze_campaign_preview_target_default_value) }, + ), + CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_location), + displayValue = devices.joinToString { it.name } + .ifEmpty { resourceProvider.getString(R.string.blaze_campaign_preview_target_default_value) }, + ), + CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_interests), + displayValue = interests.joinToString { it.description } + .ifEmpty { resourceProvider.getString(R.string.blaze_campaign_preview_target_default_value) }, + ), + ), + destinationUrl = CampaignDetailItemUi( + displayTitle = resourceProvider.getString(R.string.blaze_campaign_preview_details_destination_url), + displayValue = targetUrl, + maxLinesValue = 1, + ) + ) + + private fun BlazeRepository.Budget.toDisplayValue(): String { + val totalBudgetWithCurrency = currencyFormatter.formatCurrency( + totalBudget.toBigDecimal(), + currencyCode + ) + val duration = resourceProvider.getString( + R.string.blaze_campaign_preview_days_duration, + durationInDays, + startDate.formatToMMMdd() + ) + return "$totalBudgetWithCurrency, $duration" } + + data class CampaignPreviewUiState( + val isLoading: Boolean = false, + val adDetails: AdDetailsUi, + val campaignDetails: CampaignDetailsUi, + ) + + data class AdDetailsUi( + val productId: Long, + val title: String, + val tagLine: String, + val campaignImageUrl: String?, + ) + + data class CampaignDetailsUi( + val budget: CampaignDetailItemUi, + val targetDetails: List, + val destinationUrl: CampaignDetailItemUi, + ) + + data class CampaignDetailItemUi( + val displayTitle: String, + val displayValue: String, + val maxLinesValue: Int? = null, + ) } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 0c2d807230c..8e1af62fe3f 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3835,6 +3835,9 @@ Interests Ad destination Confirm Details + %1$s days from %2$s + All +