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 @@ -86,6 +86,16 @@ public static void init(DataProvider instance) {
*/
public abstract Observable<DataResponse> signIn(Context context, String username, String password);

/**
* Called to sign the user in using the user's external ID.
*
* @param context android context
* @param externalId the user's external ID
* @return Observable of the result of the method, with {@link DataResponse#isSuccess()}
* returning true if signIn was successful
*/
public abstract Observable<DataResponse> signInWithExternalId(Context context, String externalId);

/**
* Sign out the user. This will possibly involve a call to the server,
* and also clear all relevant local data that relates to the User, User Session, or Consent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

import org.researchstack.backbone.model.survey.factory.SurveyFactory;
import org.researchstack.backbone.step.OnboardingCompletionStep;

import java.lang.reflect.Type;

/**
Expand Down Expand Up @@ -108,14 +105,14 @@ public SurveyItem deserialize(JsonElement json, Type typeOfT, JsonDeserializatio
case ACCOUNT_REGISTRATION:
case ACCOUNT_LOGIN:
case ACCOUNT_PROFILE:
case ACCOUNT_EXTERNAL_ID:
item = context.deserialize(json, ProfileSurveyItem.class);
break;
case ACCOUNT_COMPLETION:
case ACCOUNT_EMAIL_VERIFICATION:
item = context.deserialize(json, InstructionSurveyItem.class);
break;
case ACCOUNT_DATA_GROUPS:
case ACCOUNT_EXTERNAL_ID:
case ACCOUNT_PERMISSIONS:
case PASSCODE:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.annotation.VisibleForTesting;
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
import android.text.InputType;

Expand Down Expand Up @@ -61,6 +62,7 @@
import org.researchstack.backbone.ui.step.layout.PasscodeCreationStepLayout;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

Expand All @@ -84,6 +86,16 @@ public class SurveyFactory {
public static final String PASSWORD_CONFIRMATION_IDENTIFIER = "confirmation";
public static final String CONSENT_QUIZ_IDENTIFIER = "consentQuiz";

@VisibleForTesting
static final int EXTERNAL_ID_MAX_LENGTH = 128;

private static final List<ProfileInfoOption> EXTERNAL_ID_LOGIN_OPTIONS;
static {
List<ProfileInfoOption> tempList = new ArrayList<>();
tempList.add(ProfileInfoOption.EXTERNAL_ID);
EXTERNAL_ID_LOGIN_OPTIONS = Collections.unmodifiableList(tempList);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

+1 for immutable

}

// When set, this will be used
private CustomStepCreator customStepCreator;

Expand Down Expand Up @@ -189,7 +201,7 @@ public Step createSurveyStep(Context context, SurveyItem item, boolean isSubtask
if (!(item instanceof ProfileSurveyItem)) {
throw new IllegalStateException("Error in json parsing, ACCOUNT_LOGIN types must be ProfileSurveyItem");
}
return createLoginStep(context, (ProfileSurveyItem)item);
return createLoginStep(context, (ProfileSurveyItem)item, defaultLoginOptions());
case ACCOUNT_COMPLETION:
// TODO: finish the completion step layout, for now just use a simple instruction
// TODO: should show the cool check mark animation, see iOS
Expand All @@ -207,7 +219,11 @@ public Step createSurveyStep(Context context, SurveyItem item, boolean isSubtask
case ACCOUNT_DATA_GROUPS:
return createNotImplementedStep(item);
case ACCOUNT_EXTERNAL_ID:
return createNotImplementedStep(item);
if (!(item instanceof ProfileSurveyItem)) {
throw new IllegalStateException("Error in json parsing, " +
"ACCOUNT_EXTERNAL_ID types must be ProfileSurveyItem");
}
return createLoginStep(context, (ProfileSurveyItem)item, EXTERNAL_ID_LOGIN_OPTIONS);
case PASSCODE:
return createPasscodeStep(context, item);
case SHARE_THE_APP:
Expand Down Expand Up @@ -540,7 +556,7 @@ public List<QuestionStep> createQuestionSteps(
createGenderQuestionStep(context, profileInfo);
break;
case EXTERNAL_ID:
// TODO: implement external ID step, which is used for internal app usage
questionSteps.add(createExternalIdQuestionStep(context, profileInfo));
break;
case BLOOD_TYPE: // ChoiceTextAnswerFormat, see HealthKit blood types
case FITZPATRICK_SKIN_TYPE: // ChoiceTextAnswerFormat
Expand Down Expand Up @@ -569,6 +585,22 @@ public QuestionStep createEmailQuestionStep(Context context, ProfileInfoOption p
new EmailAnswerFormat());
}

/**
* Create a question for External ID.
*
* @param context used to generate title and placeholder title for step
* @param profileOption used to set step identifier
* @return QuestionStep used for gathering user's external ID
*/
public QuestionStep createExternalIdQuestionStep(
Context context, ProfileInfoOption profileOption) {
return createGenericQuestionStep(context,
profileOption.getIdentifier(),
R.string.rsb_external_id,
R.string.rsb_external_id_placeholder,
new TextAnswerFormat(EXTERNAL_ID_MAX_LENGTH));
}

/**
* @param context used to generate title and placeholder title for step
* @param profileOption used to set step identifier
Expand Down Expand Up @@ -688,10 +720,13 @@ public ProfileStep createProfileStep(Context context, ProfileSurveyItem item) {
/**
* @param context can be any context, activity or application, used to access "R" resources
* @param item InstructionSurveyItem from JSON
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please add @param loginOptions

* @param loginOptions A list of ProfileInfoOptions representing the fields needed for login.
* Can be defaultLoginOptions() for email/password, EXTERNAL_ID_LOGIN_OPTIONS
* @return valid EmailVerificationSubStep matching the InstructionSurveyItem
*/
public LoginStep createLoginStep(Context context, ProfileSurveyItem item) {
List<ProfileInfoOption> options = createProfileInfoOptions(context, item, defaultLoginOptions());
public LoginStep createLoginStep(
Context context, ProfileSurveyItem item, List<ProfileInfoOption> loginOptions) {
List<ProfileInfoOption> options = createProfileInfoOptions(context, item, loginOptions);
return new LoginStep(
item.identifier, item.title, item.text,
options, createQuestionSteps(context, options, false)); // false = dont create ConfirmPassword step
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@
import org.researchstack.backbone.R;
import org.researchstack.backbone.model.ProfileInfoOption;
import org.researchstack.backbone.result.StepResult;
import org.researchstack.backbone.result.TaskResult;
import org.researchstack.backbone.step.QuestionStep;
import org.researchstack.backbone.step.Step;
import org.researchstack.backbone.utils.ObservableUtils;
import org.researchstack.backbone.utils.StepLayoutHelper;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import rx.Observable;

Expand Down Expand Up @@ -50,10 +46,15 @@ public void initialize(Step step, StepResult result)
{
super.initialize(step, result);

// Add the Forgot Password UI below the login form
submitBar.getNegativeActionView().setVisibility(View.VISIBLE);
submitBar.setNegativeTitle(R.string.rsb_forgot_password);
submitBar.setNegativeAction(v -> forgotPasswordClicked());
FormStepData emailStepData = getFormStepData(ProfileInfoOption.EMAIL.getIdentifier());
if (emailStepData != null) {
// Add the Forgot Password UI below the login form
// Only add this if there is an Email step in the form. This might not be present if,
// for example, we are logging in using a method other than Email.
submitBar.getNegativeActionView().setVisibility(View.VISIBLE);
submitBar.setNegativeTitle(R.string.rsb_forgot_password);
submitBar.setNegativeAction(v -> forgotPasswordClicked());
}
}

@Override
Expand All @@ -63,10 +64,25 @@ protected void onNextClicked() {
showLoadingDialog();

final String email = getEmail();
boolean hasEmail = email != null && !email.isEmpty();
final String password = getPassword();

Observable<DataResponse> login = DataProvider.getInstance()
.signIn(getContext(), email, password);
boolean hasPassword = password != null && !password.isEmpty();
final String externalId = getExternalId();
boolean hasExternalId = externalId != null && !externalId.isEmpty();

Observable<DataResponse> login;
if (hasEmail && hasPassword) {
// Login with email and password.
login = DataProvider.getInstance().signIn(getContext(), email, password);
} else if (hasExternalId) {
// Login with external ID.
login = DataProvider.getInstance().signInWithExternalId(getContext(), externalId);
} else {
// This should never happen, but if it does, fail gracefully.
hideLoadingDialog();
showOkAlertDialog("Unexpected error: No credentials provided.");
return;
}

// Only gives a callback to response on success, the rest is handled by StepLayoutHelper
StepLayoutHelper.safePerform(login, this, new StepLayoutHelper.WebCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import org.researchstack.backbone.model.User;
import org.researchstack.backbone.model.survey.factory.SurveyFactory;
import org.researchstack.backbone.result.StepResult;
import org.researchstack.backbone.result.TaskResult;
import org.researchstack.backbone.step.ProfileStep;
import org.researchstack.backbone.step.QuestionStep;
import org.researchstack.backbone.step.Step;
Expand All @@ -22,7 +21,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Created by TheMDP on 1/14/17.
Expand Down Expand Up @@ -188,6 +186,13 @@ protected QuestionStep getEmailStep() {
return getQuestionStep(ProfileInfoOption.EMAIL.getIdentifier());
}

/**
* @return External ID if this profile form step has it, null otherwise
*/
protected String getExternalId() {
return getTextAnswer(ProfileInfoOption.EXTERNAL_ID.getIdentifier());
}

/**
* @return Password if this profile form step has it, null otherwise
*/
Expand Down
2 changes: 2 additions & 0 deletions backbone/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
<string name="rsb_name_placeholder">Enter full name</string>
<string name="rsb_email">Email</string>
<string name="rsb_email_placeholder">jappleseed@example.com</string>
<string name="rsb_external_id">External ID</string>
<string name="rsb_external_id_placeholder">Enter external ID</string>
<string name="rsb_password">Password</string>
<string name="rsb_password_placeholder">Enter password</string>
<string name="rsb_confirm_password">Confirm</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
Expand Down Expand Up @@ -45,10 +42,6 @@
import org.researchstack.backbone.step.ToggleFormStep;
import org.researchstack.backbone.step.NavigationSubtaskStep;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.List;

Expand Down Expand Up @@ -147,7 +140,7 @@ public void testSurveyFactory()

assertNotNull(stepList);
assertTrue(stepList.size() > 0);
assertEquals(6, stepList.size());
assertEquals(7, stepList.size());

assertTrue(stepList.get(0) instanceof LoginStep);
assertEquals("login", stepList.get(0).getIdentifier());
Expand Down Expand Up @@ -186,6 +179,20 @@ public void testSurveyFactory()

assertTrue(stepList.get(5) instanceof InstructionStep);
assertEquals("onboardingCompletion", stepList.get(5).getIdentifier());

// This doesn't usually happen *after* onboarding completion (and never with login w/ email
// and password), but for the sake of this test, we're adding it here.
assertTrue(stepList.get(6) instanceof LoginStep);
assertEquals("externalID", stepList.get(6).getIdentifier());
LoginStep externalIdLoginStep = (LoginStep) stepList.get(6);
assertEquals(1, externalIdLoginStep.getProfileInfoOptions().size());
assertEquals(ProfileInfoOption.EXTERNAL_ID, externalIdLoginStep.getProfileInfoOptions()
.get(0));
assertEquals(1, externalIdLoginStep.getFormSteps().size());
assertTrue(externalIdLoginStep.getFormSteps().get(0).getAnswerFormat()
instanceof TextAnswerFormat);
assertEquals(SurveyFactory.EXTERNAL_ID_MAX_LENGTH, ((TextAnswerFormat) externalIdLoginStep
.getFormSteps().get(0).getAnswerFormat()).getMaximumLength());
}

@Test
Expand Down
Loading