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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

// Markdown
implementation "io.noties.markwon:core:$markwon_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@

import org.commcare.android.database.connect.models.PersonalIdSessionData;

import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;

public class PersonalIdSessionDataViewModel extends ViewModel {
private PersonalIdSessionData personalIdSessionData;
private static final String PERSONAL_ID_SESSION_DATA_KEY = "PERSONAL_ID_SESSION_DATA_KEY";
private final SavedStateHandle savedStateHandle;

public PersonalIdSessionDataViewModel(SavedStateHandle savedStateHandle) {
this.savedStateHandle = savedStateHandle;
}

/**
* Sets the session data instance.
* @param sessionData a populated PersonalIdSessionData object
*/
public void setPersonalIdSessionData(PersonalIdSessionData sessionData) {
this.personalIdSessionData = sessionData;
savedStateHandle.set(PERSONAL_ID_SESSION_DATA_KEY, sessionData);
}

/**
* Retrieves the current session data.
* @return the PersonalIdSessionData instance
*/
public PersonalIdSessionData getPersonalIdSessionData() {
return personalIdSessionData;
return savedStateHandle.get(PERSONAL_ID_SESSION_DATA_KEY);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.commcare.android.database.connect.models

import androidx.annotation.StringDef
import java.io.Serializable

/**
* Data holder for personal identification session state during Personal ID flows.
Expand Down Expand Up @@ -41,7 +42,7 @@ data class PersonalIdSessionData(
var otpFallback: Boolean = false,
// the total number of times we attempted to send the user an OTP
var otpAttempts: Int = 0,
) {
) : Serializable {
/**
* Annotation to restrict accepted authentication types used by the device.
* Only PIN or BIOMETRIC_TYPE are allowed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.commcare.utils.OtpManager.SMS_METHOD_FIREBASE;
import static org.commcare.utils.OtpManager.SMS_METHOD_PERSONAL_ID;

public class PersonalIdPhoneVerificationFragment extends BasePersonalIdFragment {
private static final String KEY_PHONE = "phone";
private static final String KEY_PHONE = "KEY_PHONE";
private static final String KEY_LAST_OTP_METHOD = "KEY_LAST_OTP_METHOD";
private static final String KEY_VERIFY_BUTTON_ENABLED = "KEY_VERIFY_BUTTON_ENABLED";
private static final String KEY_OTP_REQUEST_TIME_STRING = "KEY_OTP_REQUEST_TIME_STRING";


private Activity activity;
private String primaryPhone;
Expand All @@ -57,6 +62,7 @@ public class PersonalIdPhoneVerificationFragment extends BasePersonalIdFragment
private PersonalIdSessionData personalIdSessionData;
OtpVerificationCallback otpCallback;
private ActivityResultLauncher<Intent> smsConsentLauncher;
private String lastOtpMethod;

private final Runnable resendTimerRunnable = new Runnable() {
@Override
Expand All @@ -73,10 +79,14 @@ public void onCreate(Bundle savedInstanceState) {
personalIdSessionData = new ViewModelProvider(requireActivity())
.get(PersonalIdSessionDataViewModel.class)
.getPersonalIdSessionData();
primaryPhone = personalIdSessionData.getPhoneNumber();

if (savedInstanceState != null) {
primaryPhone = savedInstanceState.getString(KEY_PHONE);
lastOtpMethod = savedInstanceState.getString(KEY_LAST_OTP_METHOD);
} else {
primaryPhone = personalIdSessionData.getPhoneNumber();
}

initOtpManager();
}

Expand Down Expand Up @@ -139,8 +149,9 @@ public void onPersonalIdApiFailure(
binding.connectPhoneVerifyButton.setEnabled(false);
}
};
// Always fallback to Twilio (via Personal ID) if this is the second attempt in the session to send the user an OTP.
Boolean useOtpFallback = personalIdSessionData.getOtpAttempts() == 1;

// The last OTP method may be Twilio (via Personal ID) after restoring this fragment.
Boolean useOtpFallback = SMS_METHOD_PERSONAL_ID.equalsIgnoreCase(lastOtpMethod);
setupOtpManager(useOtpFallback);
}

Expand All @@ -162,19 +173,36 @@ private boolean shouldAutoSwitchToPersonalIdAuth(OtpErrorType errorType) {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = ScreenPersonalidPhoneVerifyBinding.inflate(inflater, container, false);
setupInitialState();
setupListeners();

if (savedInstanceState != null) {
restoreState(savedInstanceState);
} else {
setupInitialState();
}

setupListeners();
updateVerificationMessage();
activity.setTitle(R.string.connect_verify_phone_title);

return binding.getRoot();
}

private void setupInitialState() {
binding.connectPhoneVerifyButton.setEnabled(false);
updateVerificationMessage();
requestOtp();
}

private void restoreState(Bundle savedInstanceState) {
boolean verifyButtonEnabled = savedInstanceState.getBoolean(KEY_VERIFY_BUTTON_ENABLED);
String otpRequestTimeString = savedInstanceState.getString(KEY_OTP_REQUEST_TIME_STRING);

if (otpRequestTimeString != null) {
otpRequestTime = DateTime.parse(otpRequestTimeString);
}

binding.connectPhoneVerifyButton.setEnabled(verifyButtonEnabled);
}

private void setupListeners() {
binding.connectResendButton.setOnClickListener(v -> {
// Always fallback to Twilio (via Personal ID) if this is the first time the user reattempts to send the OTP.
Expand Down Expand Up @@ -281,6 +309,9 @@ public void onDestroyView() {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_PHONE, primaryPhone);
outState.putString(KEY_LAST_OTP_METHOD, lastOtpMethod);
outState.putBoolean(KEY_VERIFY_BUTTON_ENABLED, binding.connectPhoneVerifyButton.isEnabled());
outState.putString(KEY_OTP_REQUEST_TIME_STRING, otpRequestTime.toString());
}

private void updateVerificationMessage() {
Expand Down Expand Up @@ -374,12 +405,14 @@ private void setupOtpManager(Boolean useOtpFallback) {
otpCallback,
SMS_METHOD_PERSONAL_ID
);
lastOtpMethod = SMS_METHOD_PERSONAL_ID;
} else {
otpManager = new OtpManager(
activity,
personalIdSessionData,
otpCallback
);
lastOtpMethod = SMS_METHOD_FIREBASE;
}
}
}
1 change: 1 addition & 0 deletions app/src/org/commcare/utils/OtpManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
public class OtpManager {

public static final String SMS_METHOD_PERSONAL_ID = "personal_id";
public static final String SMS_METHOD_FIREBASE = "firebase";

private final OtpAuthService authService;

Expand Down