Skip to content
Open
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
2 changes: 2 additions & 0 deletions app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -699,5 +699,7 @@
<string name="personalid_tooltip_location_success_message">We use location data to ensure secure usage of this application.\n\n<a href="https://www.dimagi.com/terms/latest/privacy/">Click here for more info</a>.</string>

<string name="network_forbidden_error">Access denied. Please contact customer support if the problem persists.</string>
<string name="new_network_data_success">New data is available; press here to refresh.</string>
<string name="new_network_data_failure">Failed to refresh. You may be viewing outdated data.</string>
Comment on lines +702 to +703
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these messages are good, but I'd love it if we could get some input from the product team (i.e. Kim, Mary, Ashish, or Brendon) if not already


</resources>
55 changes: 55 additions & 0 deletions app/src/org/commcare/fragments/base/BaseConnectFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.viewbinding.ViewBinding
import org.commcare.dalvik.R
import org.commcare.dalvik.databinding.LoadingBinding
import org.commcare.fragments.connect.base.BaseNetworkStateViewModel
import org.commcare.fragments.connect.base.BaseNetworkStateViewModel.NetworkState.Loading
import org.commcare.interfaces.base.BaseConnectView
import org.commcare.utils.ViewUtils

abstract class BaseConnectFragment<B : ViewBinding> :
Fragment(),
Expand All @@ -19,6 +24,8 @@ abstract class BaseConnectFragment<B : ViewBinding> :
private lateinit var loadingBinding: LoadingBinding
private lateinit var rootView: View

protected var baseNetworkStateViewModel: BaseNetworkStateViewModel? = null

/**
* Implement this method in child fragments to inflate their specific binding.
*/
Expand Down Expand Up @@ -75,4 +82,52 @@ abstract class BaseConnectFragment<B : ViewBinding> :
override fun hideLoading() {
loadingBinding.root.visibility = View.GONE
}

fun startListeningToNetworkState() {
baseNetworkStateViewModel?.networkState?.observe(
getViewLifecycleOwner(),
Observer { networkState: BaseNetworkStateViewModel.NetworkState ->
when (networkState) {
is BaseNetworkStateViewModel.NetworkState.Error -> {
showNetworkFailureSnackBar()
}
Loading -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional nit: can add is here for consistency with the other branches

Suggested change
Loading -> {
is Loading -> {

}
is BaseNetworkStateViewModel.NetworkState.Success -> {
if (networkState.state) {
showNetworkSuccessSnackBar()
} else {
showNetworkFailureSnackBar()
}
}
}
},
)
}

fun showNetworkSuccessSnackBar() {
ViewUtils.showSnackBarWith(
getString(R.string.refresh),
view,
getString(R.string.new_network_data_success),
View.OnClickListener { v: View? ->
refreshUiWithNetworkData()
},
)
}

fun showNetworkFailureSnackBar() {
ViewUtils.showSnackBarWith(
getString(R.string.ok),
view,
getString(R.string.new_network_data_failure),
View.OnClickListener { v: View? ->
showOldDataOnUi()
},
)
}

open fun refreshUiWithNetworkData() {}

open fun showOldDataOnUi() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Null
} else {
message = PersonalIdOrConnectApiErrorHandler.handle(requireActivity(), errorCode, t);
}
showSnackBarWithDismissAction(getBinding().getRoot(), message);
showSnackBarWithDismissAction(getString(R.string.ok),getBinding().getRoot(), message);
FirebaseAnalyticsUtil.reportCccApiClaimJob(false);
}
}.claimJob(requireContext(), user, job.getJobId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
Expand Down Expand Up @@ -58,6 +59,7 @@ public static ConnectDeliveryProgressFragment newInstance() {
initialTabPosition = getArguments().getInt(TAB_POSITION, TAB_PROGRESS);
}

setUpViewModel();
setupTabViewPager();
setupJobCard(job);
setupRefreshAndConfirmationActions();
Expand Down Expand Up @@ -114,17 +116,28 @@ public void onTabReselected(TabLayout.Tab tab) {
});
}

private void setUpViewModel() {
setBaseNetworkStateViewModel(new ViewModelProvider(this).get(ConnectDeliveryProgressFragmentViewModel.class));
startListeningToNetworkState();
}

@Override
public void refresh() {
public void refreshUiWithNetworkData() {
updateLastUpdatedText(new Date());
updateCardMessage();
updatePaymentConfirmationTile(false);
viewPagerAdapter.refresh();
}

public void fetchDeliveryProgress() {
setWaitDialogEnabled(false);
ConnectJobHelper.INSTANCE.updateDeliveryProgress(getContext(), job, true, this, success -> {
if (success) {
updateLastUpdatedText(new Date());
updateCardMessage();
updatePaymentConfirmationTile(false);
viewPagerAdapter.refresh();
}
});
((ConnectDeliveryProgressFragmentViewModel)getBaseNetworkStateViewModel()).fetchData(requireContext(), job, this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Might be nice to have a getViewModel helper to handle retrieving the base view model and casting it, since that code is a bit clunky

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@OrangeAndGreen We might have to handle the same way as ViewBinding but as many other fragments are using the BaseConnectFragment, so might not be worth having generic type parameter now so done casting directly.

}

@Override
public void refresh() {
((ConnectDeliveryProgressFragmentViewModel)getBaseNetworkStateViewModel()).setShowLoading(true);
fetchDeliveryProgress();
}

private void setWaitDialogEnabled(boolean enabled) {
Expand Down Expand Up @@ -176,7 +189,7 @@ private void setupJobCard(ConnectJobRecord job) {
public void onResume() {
super.onResume();
if (PersonalIdManager.getInstance().isloggedIn()) {
refresh();
fetchDeliveryProgress();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.commcare.fragments.connect

import android.content.Context
import org.commcare.android.database.connect.models.ConnectJobRecord
import org.commcare.connect.ConnectActivityCompleteListener
import org.commcare.connect.ConnectJobHelper
import org.commcare.fragments.connect.base.BaseNetworkStateViewModel
import org.commcare.interfaces.base.BaseConnectView
import java.util.Date

class ConnectDeliveryProgressFragmentViewModel : BaseNetworkStateViewModel() {
var previousLastUpdatedOn: Date? = null
var showLoading = false // by default should be false, only true whenever user presses sync button

fun fetchData(
context: Context,
job: ConnectJobRecord,
view: BaseConnectView,
) {
// TODO
// lastModifiedOn = job.lastUpdate // this parameter is not coming currently from server.
launchTask {
ConnectJobHelper.updateDeliveryProgress(
context,
job,
showLoading,
view,
object : ConnectActivityCompleteListener {
override fun connectActivityComplete(success: Boolean) {
showLoading = false // back to default value
// TODO
// Real check is `success && job.lastUpdate>previousLastUpdatedOn`
// to be used whenever server starts sending lastModifiedOn
if (success) {
networkStateLiveData.value = NetworkState.Success(success)
} else {
networkStateLiveData.value = NetworkState.Error(Exception("UNKNOWN_EXCEPTION"))
}
}
},
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.commcare.fragments.connect.base

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch

open class BaseNetworkStateViewModel : ViewModel() {
sealed class NetworkState {
object Loading : NetworkState()

data class Success(
val state: Boolean,
) : NetworkState()

data class Error(
val exception: Exception,
Copy link
Contributor

Choose a reason for hiding this comment

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

Per the coderabbit comment in this file: Given the CoroutineExceptionHandler provides a Throwable instead of Exception, any reason not to store it as a Throwable here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

) : NetworkState()
}

protected val networkStateLiveData = MutableLiveData<NetworkState>()
val networkState: LiveData<NetworkState> = networkStateLiveData

protected val handler =
CoroutineExceptionHandler { _, exception ->
networkStateLiveData.value = NetworkState.Error(if (exception is Exception) exception else Exception(exception))
}

protected fun launchTask(task: suspend () -> Unit) {
viewModelScope.launch(handler) {
networkStateLiveData.value = NetworkState.Loading
task()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import static org.commcare.connect.PersonalIdManager.BIOMETRIC_INVALIDATION_KEY;
import static org.commcare.google.services.analytics.AnalyticsParamValue.CONTINUE_WITH_FINGERPRINT;
import static org.commcare.google.services.analytics.AnalyticsParamValue.CONTINUE_WITH_PIN;
import static org.commcare.utils.ViewUtils.showSnackBarWithOk;
import static org.commcare.utils.ViewUtils.showSnackBarWith;

/**
* Fragment that handles biometric or PIN verification for Connect ID authentication.
Expand Down Expand Up @@ -297,7 +297,7 @@ private void navigateForward(boolean enrollmentFailed) {
} else {
View view = getView();
if (view != null) {
showSnackBarWithOk(view, getString(R.string.connect_verify_skip_phone_number),
showSnackBarWith(getString(R.string.ok),view, getString(R.string.connect_verify_skip_phone_number),
v -> NavHostFragment.findNavController(this).navigate(navigateToNameScreen()));
}
}
Expand Down
13 changes: 7 additions & 6 deletions app/src/org/commcare/utils/ViewUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@

public class ViewUtils {
/**
* Displays a SnackBar with the given message and an "OK" button.
* The SnackBar will remain visible indefinitely until the "OK" button is pressed.
* Displays a SnackBar with the given message and an button with provided text.
* The SnackBar will remain visible indefinitely until the button is pressed.
*
* @param btnText Button text
* @param view The view to find a parent from. This view is used to create the SnackBar.
* @param message The message text to show in the SnackBar.
* @param okClickListener The callback to be invoked when the "OK" button is clicked.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Javadoc references hardcoded "OK" button.

The @param okClickListener description still says "OK" button, but the method now supports customizable button text via btnText.

-     * @param okClickListener The callback to be invoked when the "OK" button is clicked.
+     * @param okClickListener The callback to be invoked when the action button is clicked.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param okClickListener The callback to be invoked when the "OK" button is clicked.
* @param okClickListener The callback to be invoked when the action button is clicked.
🤖 Prompt for AI Agents
In app/src/org/commcare/utils/ViewUtils.java around line 17, the Javadoc for the
parameter okClickListener still references a hardcoded "OK" button even though
the method now accepts customizable btnText; update the @param description to
refer to the positive/confirmation button (or the button with customizable text)
rather than "OK", e.g. say "The callback invoked when the positive/confirmation
button is clicked (button text is provided via btnText)"; ensure the phrasing
matches the method's parameter names and behavior.

*/
public static void showSnackBarWithOk(View view, String message, View.OnClickListener okClickListener) {
public static void showSnackBarWith(String btnText, View view, String message, View.OnClickListener okClickListener) {
Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE)
.setAction("OK", okClickListener)
.setAction(btnText, okClickListener)
.show();
}
Comment on lines 10 to 23
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we refactor this function (and its comment) to not reference "ok" for the click listener (i.e. clickListener and not okClickListener) since the button text is not always "OK".

public static void showSnackBarWithDismissAction(View view, String message) {
public static void showSnackBarWithDismissAction(String btnText,View view, String message) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE);
snackbar.setAction(view.getContext().getString(R.string.ok),v -> snackbar.dismiss());
snackbar.setAction(btnText,v -> snackbar.dismiss());
snackbar.show();
}
}