Skip to content

Commit

Permalink
Merge branch 'trunk' into 12496-totals-calculations-ui-issues
Browse files Browse the repository at this point in the history
  • Loading branch information
backwardstruck authored Sep 12, 2024
2 parents 608e605 + b3f80f7 commit 1b926d2
Show file tree
Hide file tree
Showing 20 changed files with 351 additions and 55 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
20.4
-----
- [**] Users can now scan their tracking number when adding it to the order [https://github.com/woocommerce/woocommerce-android/pull/12533]
- [*] Fixed an issue where shipping labels were incorrectly calculating the weight for packages containing multiple quantities of the same product [https://github.com/woocommerce/woocommerce-android/pull/12602]

20.3
-----
Expand Down
19 changes: 15 additions & 4 deletions WooCommerce/src/main/kotlin/com/woocommerce/android/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ object AppPrefs {
STORE_CREATION_PROFILER_ANSWERS,
AI_CONTENT_GENERATION_TONE,
AI_PRODUCT_CREATION_IS_FIRST_ATTEMPT,
BLAZE_FIRST_TIME_WITHOUT_CAMPAIGN,
BLAZE_CAMPAIGN_CREATED,
BLAZE_CELEBRATION_SCREEN_SHOWN,
BLAZE_NO_CAMPAIGN_REMINDER_SHOWN,
Expand Down Expand Up @@ -1047,15 +1048,25 @@ object AppPrefs {
value = value
)

fun setBlazeCampaignCreated(siteId: Long) {
var blazeFirstTimeWithoutCampaign: Long
get() = getLong(DeletablePrefKey.BLAZE_FIRST_TIME_WITHOUT_CAMPAIGN, 0L)
set(value) = setLong(DeletablePrefKey.BLAZE_FIRST_TIME_WITHOUT_CAMPAIGN, value)

fun removeBlazeFirstTimeWithoutCampaign() {
remove(DeletablePrefKey.BLAZE_FIRST_TIME_WITHOUT_CAMPAIGN)
}

fun existsBlazeFirstTimeWithoutCampaign() = exists(DeletablePrefKey.BLAZE_FIRST_TIME_WITHOUT_CAMPAIGN)

fun setBlazeCampaignCreated() {
setBoolean(
key = PrefKeyString("$BLAZE_CAMPAIGN_CREATED:$siteId"),
key = PrefKeyString("$BLAZE_CAMPAIGN_CREATED"),
value = true
)
}

fun getBlazeCampaignCreated(siteId: Long) = getBoolean(
key = PrefKeyString("$BLAZE_CAMPAIGN_CREATED:$siteId"),
fun getBlazeCampaignCreated() = getBoolean(
key = PrefKeyString("$BLAZE_CAMPAIGN_CREATED"),
default = false
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class AppPrefsWrapper @Inject constructor() {

var isBlazeCelebrationScreenShown by AppPrefs::isBlazeCelebrationScreenShown

var blazeFirstTimeWithoutCampaign by AppPrefs::blazeFirstTimeWithoutCampaign

var isBlazeNoCampaignReminderShown by AppPrefs::isBlazeNoCampaignReminderShown

var isBlazeAbandonedCampaignReminderShown by AppPrefs::isBlazeAbandonedCampaignReminderShown
Expand Down Expand Up @@ -376,9 +378,15 @@ class AppPrefsWrapper @Inject constructor() {
fun getNotificationChannelTypeSuffix(channel: NotificationChannelType): Int? =
AppPrefs.getNotificationChannelTypeSuffix(channel)

fun setBlazeCampaignCreated(siteId: Long) {
AppPrefs.setBlazeCampaignCreated(siteId)
fun removeBlazeFirstTimeWithoutCampaign() {
AppPrefs.removeBlazeFirstTimeWithoutCampaign()
}

fun existsBlazeFirstTimeWithoutCampaign() = AppPrefs.existsBlazeFirstTimeWithoutCampaign()

fun setBlazeCampaignCreated() {
AppPrefs.setBlazeCampaignCreated()
}

fun getBlazeCampaignCreated(siteId: Long) = AppPrefs.getBlazeCampaignCreated(siteId)
fun getBlazeCampaignCreated() = AppPrefs.getBlazeCampaignCreated()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.work.WorkManager
import androidx.work.workDataOf
import com.woocommerce.android.analytics.AnalyticsEvent
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.viewmodel.ResourceProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
Expand Down Expand Up @@ -47,6 +48,11 @@ class LocalNotificationScheduler @Inject constructor(
AnalyticsTracker.KEY_BLOG_ID to notification.siteId,
)
)
WooLog.d(
tag = WooLog.T.NOTIFICATIONS,
message = "Local notification scheduled: " +
"type=${notification.type}, delay=${notification.delay}${notification.delayUnit}"
)
}

private fun buildPreconditionCheckWorkRequest(notification: LocalNotification): OneTimeWorkRequest {
Expand Down Expand Up @@ -79,5 +85,9 @@ class LocalNotificationScheduler @Inject constructor(

fun cancelScheduledNotification(type: LocalNotificationType) {
workManager.cancelAllWorkByTag(type.value)
WooLog.d(
tag = WooLog.T.NOTIFICATIONS,
message = "Local notification canceled: $type"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class LocalNotificationWorker @AssistedInject constructor(
when (LocalNotificationType.fromString(type)) {
LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER -> {
appsPrefsWrapper.isBlazeNoCampaignReminderShown = true
appsPrefsWrapper.removeBlazeFirstTimeWithoutCampaign()
}

LocalNotificationType.BLAZE_ABANDONED_CAMPAIGN_REMINDER -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ class AbandonedCampaignReminder @Inject constructor(
get() = BlazeAbandonedCampaignReminderNotification(selectedSite.get().siteId)

fun scheduleReminderIfNeeded() {
if (!appPrefsWrapper.getBlazeCampaignCreated(selectedSite.get().siteId) &&
!appPrefsWrapper.isBlazeAbandonedCampaignReminderShown
) {
if (!appPrefsWrapper.getBlazeCampaignCreated() && !appPrefsWrapper.isBlazeAbandonedCampaignReminderShown) {
localNotificationScheduler.scheduleNotification(notification)
}
}

fun setBlazeCampaignCreated() {
appPrefsWrapper.setBlazeCampaignCreated(selectedSite.get().siteId)
appPrefsWrapper.setBlazeCampaignCreated()
localNotificationScheduler.cancelScheduledNotification(notification.type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import com.woocommerce.android.AppPrefsWrapper
import com.woocommerce.android.extensions.daysLater
import com.woocommerce.android.notifications.local.LocalNotification.BlazeNoCampaignReminderNotification
import com.woocommerce.android.notifications.local.LocalNotificationScheduler
import com.woocommerce.android.notifications.local.LocalNotificationType
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.blaze.CampaignStatusUi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
Expand All @@ -13,6 +15,7 @@ import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.blaze.BlazeCampaignModel
import org.wordpress.android.fluxc.store.blaze.BlazeCampaignsStore
import java.util.Calendar
import java.util.concurrent.TimeUnit
import javax.inject.Inject

/**
Expand All @@ -36,29 +39,57 @@ class BlazeCampaignsObserver @Inject constructor(
private suspend fun observeBlazeCampaigns(site: SiteModel) {
blazeCampaignsStore.observeBlazeCampaigns(site)
.filter { it.isNotEmpty() }
.collectLatest { scheduleNotification(it) }
.collectLatest { processCampaigns(it) }
}

private fun scheduleNotification(campaigns: List<BlazeCampaignModel>) {
private fun processCampaigns(campaigns: List<BlazeCampaignModel>) {
if (campaigns.isEmpty()) {
// There are no campaigns. Skip scheduling the notification.
return
} else if (hasActiveEndlessCampaigns(campaigns)) {
appPrefsWrapper.removeBlazeFirstTimeWithoutCampaign()
localNotificationScheduler.cancelScheduledNotification(LocalNotificationType.BLAZE_NO_CAMPAIGN_REMINDER)
} else if (campaigns.any { CampaignStatusUi.isActive(it.uiStatus) }) {
// There are active limited campaigns.
val latestEndTime = getLatestEndTimeOfActiveLimitedCampaigns(campaigns)
scheduleNotification(latestEndTime)
} else if (!appPrefsWrapper.existsBlazeFirstTimeWithoutCampaign() ||
appPrefsWrapper.blazeFirstTimeWithoutCampaign > Calendar.getInstance().time.time
) {
scheduleNotification(Calendar.getInstance().time.time)
}
}

val delayForNotification = calculateDelayForNotification(campaigns)
private fun hasActiveEndlessCampaigns(campaigns: List<BlazeCampaignModel>) = campaigns.any {
it.isEndlessCampaign && CampaignStatusUi.isActive(it.uiStatus)
}

localNotificationScheduler.scheduleNotification(
BlazeNoCampaignReminderNotification(selectedSite.get().siteId, delayForNotification)
)
private fun getLatestEndTimeOfActiveLimitedCampaigns(campaigns: List<BlazeCampaignModel>): Long {
val activeLimitedCampaigns = campaigns.filter {
!it.isEndlessCampaign && CampaignStatusUi.isActive(it.uiStatus)
}
return activeLimitedCampaigns.maxOf { it.startTime.daysLater(it.durationInDays) }.time
}

private fun calculateDelayForNotification(campaigns: List<BlazeCampaignModel>): Long {
val latestEndTime = campaigns.maxOf { it.startTime.daysLater(it.durationInDays) }
val notificationTime = latestEndTime.daysLater(DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION)
return notificationTime.time - Calendar.getInstance().time.time
private fun scheduleNotification(firstTimeWithoutCampaign: Long) {
if (appPrefsWrapper.blazeFirstTimeWithoutCampaign == firstTimeWithoutCampaign) {
// There is already a scheduled notification for firstTimeWithoutCampaign.
return
}
appPrefsWrapper.blazeFirstTimeWithoutCampaign = firstTimeWithoutCampaign

val notificationTime = firstTimeWithoutCampaign +
TimeUnit.DAYS.toMillis(DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION)

val notification = BlazeNoCampaignReminderNotification(
siteId = selectedSite.get().siteId,
delay = notificationTime - Calendar.getInstance().time.time
)

localNotificationScheduler.scheduleNotification(notification)
}

companion object {
const val DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION = 30
private const val DAYS_DURATION_NO_CAMPAIGN_REMINDER_NOTIFICATION = 30L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Outlined
import androidx.compose.material.icons.outlined.MoreVert
Expand All @@ -30,7 +32,8 @@ fun <T> WCOverflowMenu(
onSelected: (T) -> Unit,
modifier: Modifier = Modifier,
mapper: @Composable (T) -> String = { it.toString() },
tint: Color = Color.Black
itemColor: @Composable (T) -> Color = { LocalContentColor.current },
tint: Color = MaterialTheme.colors.primary
) {
var showMenu by remember { mutableStateOf(false) }
Box(modifier = modifier) {
Expand All @@ -57,7 +60,10 @@ fun <T> WCOverflowMenu(
onSelected(item)
}
) {
Text(mapper(item))
Text(
text = mapper(item),
color = itemColor(item)
)
}
if (index < items.size - 1) {
Spacer(modifier = Modifier.height(dimensionResource(id = dimen.minor_100)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class CustomFieldsEditorFragment : BaseFragment() {
companion object {
const val RESULT_KEY = "custom_field_result"
}

override val activityAppBarStatus: AppBarStatus = AppBarStatus.Hidden

private val viewModel: CustomFieldsEditorViewModel by viewModels()
Expand All @@ -36,7 +32,7 @@ class CustomFieldsEditorFragment : BaseFragment() {
private fun handleEvents() {
viewModel.event.observe(viewLifecycleOwner) { event ->
when (event) {
is MultiLiveEvent.Event.ExitWithResult<*> -> navigateBackWithResult(RESULT_KEY, event.data)
is MultiLiveEvent.Event.ExitWithResult<*> -> navigateBackWithResult(event.key!!, event.data)
MultiLiveEvent.Event.Exit -> findNavController().navigateUp()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
Expand All @@ -18,6 +19,7 @@ import com.woocommerce.android.R
import com.woocommerce.android.ui.compose.component.DiscardChangesDialog
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.component.WCOutlinedTextField
import com.woocommerce.android.ui.compose.component.WCOverflowMenu
import com.woocommerce.android.ui.compose.component.WCTextButton
import com.woocommerce.android.ui.compose.component.aztec.OutlinedAztecEditor
import com.woocommerce.android.ui.compose.component.getText
Expand All @@ -33,6 +35,7 @@ fun CustomFieldsEditorScreen(viewModel: CustomFieldsEditorViewModel) {
onKeyChanged = viewModel::onKeyChanged,
onValueChanged = viewModel::onValueChanged,
onDoneClicked = viewModel::onDoneClicked,
onDeleteClicked = viewModel::onDeleteClicked,
onBackButtonClick = viewModel::onBackClick,
)
}
Expand All @@ -44,6 +47,7 @@ private fun CustomFieldsEditorScreen(
onKeyChanged: (String) -> Unit,
onValueChanged: (String) -> Unit,
onDoneClicked: () -> Unit,
onDeleteClicked: () -> Unit,
onBackButtonClick: () -> Unit,
) {
BackHandler { onBackButtonClick() }
Expand All @@ -60,6 +64,24 @@ private fun CustomFieldsEditorScreen(
text = stringResource(R.string.done)
)
}
if (!state.isCreatingNewItem) {
WCOverflowMenu(
items = listOf(R.string.delete),
mapper = { stringResource(it) },
itemColor = {
when (it) {
R.string.delete -> MaterialTheme.colors.error
else -> LocalContentColor.current
}
},
onSelected = { resourceId ->
when (resourceId) {
R.string.delete -> onDeleteClicked()
else -> error("Unhandled menu item")
}
}
)
}
}
)
},
Expand Down Expand Up @@ -117,6 +139,7 @@ private fun CustomFieldsEditorScreenPreview() {
onKeyChanged = {},
onValueChanged = {},
onDoneClicked = {},
onDeleteClicked = {},
onBackButtonClick = {}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class CustomFieldsEditorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: CustomFieldsRepository
) : ScopedViewModel(savedStateHandle) {
companion object {
const val CUSTOM_FIELD_UPDATED_RESULT_KEY = "custom_field_updated"
const val CUSTOM_FIELD_DELETED_RESULT_KEY = "custom_field_deleted"
}

private val navArgs by savedStateHandle.navArgs<CustomFieldsEditorFragmentArgs>()

private val customFieldDraft = savedStateHandle.getStateFlow(
Expand Down Expand Up @@ -57,7 +62,8 @@ class CustomFieldsEditorViewModel @Inject constructor(
storedValue?.value.orEmpty() != customField.value,
isHtml = isHtml,
discardChangesDialogState = discardChangesDialogState,
keyErrorMessage = keyErrorMessage
keyErrorMessage = keyErrorMessage,
isCreatingNewItem = storedValue == null
)
}.asLiveData()

Expand All @@ -84,15 +90,25 @@ class CustomFieldsEditorViewModel @Inject constructor(
if (existingFields.any { it.key == value.key }) {
keyErrorMessage.value = UiString.UiStringRes(R.string.custom_fields_editor_key_error_duplicate)
} else {
triggerEvent(MultiLiveEvent.Event.ExitWithResult(value))
triggerEvent(
MultiLiveEvent.Event.ExitWithResult(data = value, key = CUSTOM_FIELD_UPDATED_RESULT_KEY)
)
}
}
} else {
// When editing, we don't need to check for duplicate keys
triggerEvent(MultiLiveEvent.Event.ExitWithResult(value))
triggerEvent(
MultiLiveEvent.Event.ExitWithResult(data = value, key = CUSTOM_FIELD_UPDATED_RESULT_KEY)
)
}
}

fun onDeleteClicked() {
triggerEvent(
MultiLiveEvent.Event.ExitWithResult(data = navArgs.customField, key = CUSTOM_FIELD_DELETED_RESULT_KEY)
)
}

fun onBackClick() {
if (state.value?.hasChanges == true) {
showDiscardChangesDialog.value = true
Expand All @@ -118,6 +134,7 @@ class CustomFieldsEditorViewModel @Inject constructor(
val isHtml: Boolean = false,
val discardChangesDialogState: DiscardChangesDialogState? = null,
val keyErrorMessage: UiString? = null,
val isCreatingNewItem: Boolean = false
) {
val showDoneButton
get() = customField.key.isNotEmpty() && hasChanges && keyErrorMessage == null
Expand Down
Loading

0 comments on commit 1b926d2

Please sign in to comment.