/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.waz.zclient.core.api.scala; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import com.waz.api.AccentColor; import com.waz.api.CoreList; import com.waz.api.Credentials; import com.waz.api.CredentialsFactory; import com.waz.api.CredentialsUpdateListener; import com.waz.api.ErrorResponse; import com.waz.api.ErrorsList; import com.waz.api.ImageAsset; import com.waz.api.InvitationTokenFactory; import com.waz.api.Invitations; import com.waz.api.KindOfAccess; import com.waz.api.KindOfVerification; import com.waz.api.LoginListener; import com.waz.api.OtrClient; import com.waz.api.Self; import com.waz.api.UpdateListener; import com.waz.api.ZMessagingApi; import com.waz.zclient.core.controllers.tracking.attributes.OutcomeAttribute; import com.waz.zclient.core.controllers.tracking.attributes.RegistrationEventContext; import com.waz.zclient.core.controllers.tracking.events.registration.EditSelfUser; import com.waz.zclient.core.controllers.tracking.events.registration.EmailVerification; import com.waz.zclient.core.controllers.tracking.events.registration.EnteredEmailAndPasswordEvent; import com.waz.zclient.core.controllers.tracking.events.registration.EnteredNameEvent; import com.waz.zclient.core.controllers.tracking.events.registration.EnteredPhoneEvent; import com.waz.zclient.core.controllers.tracking.events.registration.OpenedEmailSignUpEvent; import com.waz.zclient.core.controllers.tracking.events.registration.OpenedPhoneSignUpEvent; import com.waz.zclient.core.controllers.tracking.events.registration.PhoneVerification; import com.waz.zclient.core.controllers.tracking.events.registration.RequestedPhoneVerificationCallEvent; import com.waz.zclient.core.controllers.tracking.events.registration.ResentEmailVerificationEvent; import com.waz.zclient.core.controllers.tracking.events.registration.ResentPhoneVerificationEvent; import com.waz.zclient.core.controllers.tracking.events.registration.SucceededWithRegistrationEvent; import com.waz.zclient.core.controllers.tracking.events.registration.VerifiedEmailEvent; import com.waz.zclient.core.controllers.tracking.events.registration.VerifiedPhoneEvent; import com.waz.zclient.core.controllers.tracking.events.session.LoggedInEvent; import com.waz.zclient.core.stores.appentry.AppEntryError; import com.waz.zclient.core.stores.appentry.AppEntryState; import com.waz.zclient.core.stores.appentry.AppEntryStateCallback; import com.waz.zclient.core.stores.appentry.IAppEntryStore; import com.waz.zclient.utils.LayoutSpec; import timber.log.Timber; public class AppEntryStore implements IAppEntryStore, ErrorsList.ErrorListener { public static final String TAG = AppEntryStore.class.getName(); public static final String GENERAL_GENERIC_INVITE_TOKEN = "getwire"; private static final String PREF_REGISTRATION = "PREF_REGISTRATION"; private static final String PREF_ITEM_ENTRY_POINT = "PREF_ITEM_ENTRY_POINT"; private static final String PREF_ITEM_EMAIL = "PREF_ITEM_EMAIL"; private static final String PREF_ITEM_COUNTRY_CODE = "PREF_ITEM_COUNTRY_CODE"; private static final String PREF_ITEM_PHONE_VERIFICATION_CODE = "PREF_ITEM_PHONE_VERIFICATION_CODE"; private static final String PREF_ITEM_PHONE = "PREF_ITEM_PHONE"; private static final String PREF_ITEM_NAME = "PREF_ITEM_NAME"; private static final String SAVED_INSTANCE_CURRENT_STATE = "SAVED_INSTANCE_CURRENT_STATE"; private Context context; private Self self; private CoreList<OtrClient> otherOtrClients; private ZMessagingApi zMessagingApi; private ErrorsList errors; private AppEntryStateCallback appEntryStateCallback; private AppEntryState currentState; private AppEntryState entryPoint; private boolean ignoreSelfUpdates; private String countryCode; private String phone; private String phoneVerificationCode; private String name; private String email; private String password; private String invitationEmail; private String invitationName; private String invitationPhone; private Invitations.PersonalToken invitationToken; private RegistrationEventContext registrationEventContext; private UpdateListener selfUpdateListener = new UpdateListener() { @Override public void updated() { onSelfUpdated(); } }; private UpdateListener otrClientsUpdateListender = new UpdateListener() { @Override public void updated() { Timber.i("otrClientsUpdateListender size=%d", otherOtrClients.size()); } }; public AppEntryStore(Context context, ZMessagingApi zMessagingApi) { this.context = context; this.zMessagingApi = zMessagingApi; errors = zMessagingApi.getErrors(); errors.addErrorListener(this); SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE); if (sharedPreferences.contains(PREF_ITEM_ENTRY_POINT)) { entryPoint = AppEntryState.values()[sharedPreferences.getInt(PREF_ITEM_ENTRY_POINT, -1)]; } countryCode = sharedPreferences.getString(PREF_ITEM_COUNTRY_CODE, null); phone = sharedPreferences.getString(PREF_ITEM_PHONE, null); email = sharedPreferences.getString(PREF_ITEM_EMAIL, null); name = sharedPreferences.getString(PREF_ITEM_NAME, null); phoneVerificationCode = sharedPreferences.getString(PREF_ITEM_PHONE_VERIFICATION_CODE, null); } public void resumeAppEntry(Self self, String personalInvitationToken) { if (!bindSelf(self)) { return; } if (!TextUtils.isEmpty(personalInvitationToken)) { invitationToken = InvitationTokenFactory.personalTokenFromCode(personalInvitationToken); zMessagingApi.getInvitations().retrieveInvitationDetails( invitationToken, new Invitations.InvitationDetailsCallback() { @Override public void onEmailAdressRetrieved(String name, String email) { invitationName = name; invitationEmail = email; setState(AppEntryState.EMAIL_INVITATION); } @Override public void onPhoneNumberRetrieved(String name, String phone) { invitationName = name; invitationPhone = phone; setState(AppEntryState.PHONE_INVITATION); } @Override public void onRetrievalFailed(ErrorResponse errorResponse) { if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } } }); return; } // Resume phone registration if (entryPoint == AppEntryState.PHONE_REGISTER && self.isLoggedIn()) { if (self.getPicture().isEmpty()) { setState(AppEntryState.PHONE_SET_PICTURE); return; } setState(AppEntryState.LOGGED_IN); return; } //Resume at phone set name if (entryPoint == AppEntryState.PHONE_SET_NAME && self.isLoggedIn()) { setState(AppEntryState.PHONE_SET_NAME); return; } // Resume phone sign-in if (entryPoint == AppEntryState.PHONE_SIGN_IN && self.isLoggedIn()) { setState(AppEntryState.PHONE_SIGNED_IN_RESUMING); return; } // Resume email registration if (entryPoint == AppEntryState.EMAIL_REGISTER) { if (self.isLoggedIn() && !self.accountActivated()) { setState(AppEntryState.EMAIL_VERIFY_EMAIL); return; } if (self.accountActivated() && self.getPicture().isEmpty()) { setState(AppEntryState.EMAIL_SET_PICTURE); return; } if (self.accountActivated()) { appEntryStateCallback.tagAppEntryEvent(new VerifiedEmailEvent(OutcomeAttribute.SUCCESS, "", getEmailRegistrationContext())); appEntryStateCallback.tagAppEntryEvent(new SucceededWithRegistrationEvent(getEmailRegistrationContext())); setState(AppEntryState.LOGGED_IN); return; } } // Resume email sign-in if (entryPoint == AppEntryState.EMAIL_SIGN_IN && self.isLoggedIn()) { setState(AppEntryState.EMAIL_SIGNED_IN); return; } // Start registration if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } } @Override public void clearCurrentState() { bindSelf(null); currentState = null; } @Override public void clearSavedUserInput() { countryCode = null; phone = null; name = null; email = null; password = null; phoneVerificationCode = null; context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE) .edit() .remove(PREF_ITEM_COUNTRY_CODE) .remove(PREF_ITEM_PHONE) .remove(PREF_ITEM_NAME) .remove(PREF_ITEM_EMAIL) .remove(PREF_ITEM_PHONE_VERIFICATION_CODE) .apply(); } // Here we handle email verification click on a different device private void onSelfUpdated() { if (otherOtrClients == null || otherOtrClients.size() == 0) { if (otherOtrClients != null) { otherOtrClients.removeUpdateListener(otrClientsUpdateListender); } otherOtrClients = self.getOtherOtrClients(); otherOtrClients.addUpdateListener(otrClientsUpdateListender); } if (ignoreSelfUpdates || appEntryStateCallback == null) { return; } if (currentState == AppEntryState.PHONE_EMAIL_PASSWORD) { return; } if (entryPoint == AppEntryState.PHONE_SIGN_IN && self.accountActivated()) { appEntryStateCallback.tagAppEntryEvent(EmailVerification.success(EmailVerification.Context.POST_LOGIN)); if (self.getPicture().isEmpty()) { setState(AppEntryState.PHONE_SET_PICTURE); return; } setState(AppEntryState.LOGGED_IN); return; } if (entryPoint == AppEntryState.EMAIL_REGISTER && self.accountActivated()) { if (currentState == AppEntryState.EMAIL_VERIFY_EMAIL) { // Check state so following registration events are triggered only once appEntryStateCallback.tagAppEntryEvent(new VerifiedEmailEvent(OutcomeAttribute.SUCCESS, "", getEmailRegistrationContext())); appEntryStateCallback.tagAppEntryEvent(new SucceededWithRegistrationEvent(getEmailRegistrationContext())); } if (self.getPicture().isEmpty()) { setState(AppEntryState.EMAIL_SET_PICTURE); return; } setState(AppEntryState.LOGGED_IN); } } /* returns true if bound to new self */ private boolean bindSelf(Self self) { // always unregister old self if (this.self != null && this.self != self) { this.self.removeUpdateListener(selfUpdateListener); if (otherOtrClients != null) { otherOtrClients.removeUpdateListener(otrClientsUpdateListender); } } else if (this.self == self) { return false; } if (self == null) { this.self = null; otherOtrClients = null; return false; } this.self = self; this.self.addUpdateListener(selfUpdateListener); otherOtrClients = self.getOtherOtrClients(); otherOtrClients.addUpdateListener(otrClientsUpdateListender); return true; } //////////////////////// // State Machine //////////////////////// @Override public boolean onBackPressed() { if (currentState == null) { return false; } switch (currentState) { case PHONE_SIGN_IN: case PHONE_INVITATION: case EMAIL_INVITATION: if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } return true; case PHONE_SET_CODE: if (entryPoint == AppEntryState.EMAIL_SIGN_IN) { setState(AppEntryState.EMAIL_SET_PHONE); } else { setState(entryPoint); } return true; case PHONE_VERIFY_EMAIL: setState(AppEntryState.PHONE_EMAIL_PASSWORD); return true; case EMAIL_VERIFY_EMAIL: setState(AppEntryState.EMAIL_REGISTER); return true; case EMAIL_REGISTER: setState(AppEntryState.EMAIL_WELCOME); return true; case EMAIL_SIGN_IN: if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } return true; case EMAIL_SET_CODE: setState(AppEntryState.EMAIL_SET_PHONE); return true; case PHONE_SET_PICTURE: setState(AppEntryState.PHONE_SET_NAME); return true; default: return false; } } @Override public void tearDown() { bindSelf(null); errors.removeErrorListener(this); errors = null; zMessagingApi = null; context = null; } @Override public void setState(AppEntryState state) { setStateInternal(state, false); } @Override public void triggerStateUpdate() { if (currentState == null) { return; } setStateInternal(currentState, true); } private void setStateInternal(AppEntryState state, boolean forceUpdate) { if (appEntryStateCallback == null) { return; } if (currentState == state && !forceUpdate) { return; } currentState = state; if (AppEntryState.entryPoints().contains(state)) { persistAppEntryPoint(state); } switch (state) { case PHONE_REGISTER: appEntryStateCallback.onShowPhoneRegistrationPage(); break; case PHONE_SIGN_IN: appEntryStateCallback.onShowPhoneSignInPage(); break; case PHONE_SET_CODE: appEntryStateCallback.onShowPhoneCodePage(); break; case PHONE_SIGNED_IN: if (self.getEmail().isEmpty() && otherOtrClients != null && otherOtrClients.size() > 0) { setState(AppEntryState.PHONE_EMAIL_PASSWORD); break; } if (self.getPicture().isEmpty()) { setState(AppEntryState.PHONE_SET_PICTURE); break; } setState(AppEntryState.LOGGED_IN); break; // TODO: This state was needed because SyncEngine isEmailVerified() was not entirely reliable, // accountActivated() flag should not have this problem, we should remove this special state. case PHONE_SIGNED_IN_RESUMING: if (self.getEmail().isEmpty() && otherOtrClients != null && otherOtrClients.size() > 0) { setState(AppEntryState.PHONE_EMAIL_PASSWORD); break; } if (!self.accountActivated()) { setState(AppEntryState.PHONE_VERIFY_EMAIL); break; } if (self.getPicture().isEmpty()) { setState(AppEntryState.PHONE_SET_PICTURE); break; } setState(AppEntryState.FIRST_LOGIN); break; case PHONE_EMAIL_PASSWORD: appEntryStateCallback.onShowPhoneAddEmailPage(); break; case PHONE_VERIFY_EMAIL: appEntryStateCallback.onShowPhoneVerifyEmailPage(); break; case PHONE_SET_NAME: appEntryStateCallback.onShowPhoneNamePage(); break; case PHONE_SET_PICTURE: appEntryStateCallback.onShowPhoneSetPicturePage(); break; case EMAIL_WELCOME: appEntryStateCallback.onShowEmailWelcomePage(); break; case EMAIL_REGISTER: appEntryStateCallback.onShowEmailRegistrationPage(); break; case EMAIL_SIGN_IN: appEntryStateCallback.onShowEmailSignInPage(); break; case EMAIL_VERIFY_EMAIL: appEntryStateCallback.onShowEmailVerifyEmailPage(); break; case EMAIL_SET_PICTURE: appEntryStateCallback.onShowEmailSetPicturePage(); break; case EMAIL_SET_PHONE: appEntryStateCallback.onShowEmailAddPhonePage(); break; case EMAIL_SET_CODE: appEntryStateCallback.onShowEmailPhoneCodePage(); break; case EMAIL_SIGNED_IN: if (!self.accountActivated()) { setState(AppEntryState.EMAIL_VERIFY_EMAIL); break; } if (self.getPicture().isEmpty()) { setState(AppEntryState.EMAIL_SET_PICTURE); break; } if (self.getPhone().isEmpty()) { setState(AppEntryState.EMAIL_SET_PHONE); break; } setState(AppEntryState.FIRST_LOGIN); break; case LOGGED_IN: entryPoint = null; context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().clear().apply(); self.removeUpdateListener(selfUpdateListener); if (otherOtrClients != null) { otherOtrClients.removeUpdateListener(otrClientsUpdateListender); } appEntryStateCallback.onEnterApplication(); break; case EMAIL_INVITATION: appEntryStateCallback.onShowEmailInvitationPage(); setRegistrationContext(RegistrationEventContext.PERSONAL_INVITE_EMAIL); break; case PHONE_INVITATION: appEntryStateCallback.onShowPhoneInvitationPage(); setRegistrationContext(RegistrationEventContext.PERSONAL_INVITE_PHONE); break; case FIRST_LOGIN: appEntryStateCallback.onShowFirstLaunchPage(); break; } } private void persistAppEntryPoint(AppEntryState state) { entryPoint = state; context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE) .edit() .putInt(PREF_ITEM_ENTRY_POINT, entryPoint.ordinal()) .apply(); } @Override public void setCallback(AppEntryStateCallback callback) { appEntryStateCallback = callback; } @Override public void onSaveInstanceState(Bundle outState) { outState.putSerializable(SAVED_INSTANCE_CURRENT_STATE, currentState); } @Override public void onRestoreInstanceState(Bundle savedInstanceState, Self self) { // Shouldn't setState() or resumeAppEntry(), as the correct fragment should already be displayed currentState = (AppEntryState) savedInstanceState.getSerializable(SAVED_INSTANCE_CURRENT_STATE); bindSelf(self); } @Override public AppEntryState getEntryPoint() { return entryPoint; } //////////////////////// // // Calls from UI // //////////////////////// @Override public void setRegistrationPhone(final String countryCode, final String phone, final ErrorCallback errorCallback) { setAndStoreCountryCode(countryCode); setAndStorePhone(phone); zMessagingApi.requestPhoneConfirmationCode(countryCode + phone, KindOfAccess.REGISTRATION, new ZMessagingApi.PhoneConfirmationCodeRequestListener() { @Override public void onConfirmationCodeSent(KindOfAccess kindOfAccess) { appEntryStateCallback.tagAppEntryEvent(new EnteredPhoneEvent( OutcomeAttribute.SUCCESS, "", "", getPhoneRegistrationContext())); setState(AppEntryState.PHONE_SET_CODE); } @Override public void onPasswordExists(KindOfAccess kindOfAccess) { } @Override public void onConfirmationCodeSendingFailed(KindOfAccess kindOfAccess, int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(new EnteredPhoneEvent( OutcomeAttribute.FAIL, String.valueOf(errorCode), message + "; " + label, getPhoneRegistrationContext())); // This phone number is already registered, redirect to sign-in if (errorCode == AppEntryError.PHONE_EXISTS.errorCode) { persistAppEntryPoint(AppEntryState.PHONE_SIGN_IN); setSignInPhone(countryCode, phone, errorCallback); // Pass error to UI } else if (AppEntryError.PHONE_INVALID_FORMAT.correspondsTo(errorCode, label)) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorReg()); errorCallback.onError(AppEntryError.PHONE_INVALID_FORMAT); } else if (AppEntryError.PHONE_BUDGET_EXHAUSTED.correspondsTo(errorCode, label)) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorReg()); errorCallback.onError(AppEntryError.PHONE_BUDGET_EXHAUSTED); } else { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorReg()); errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } @Override public void setSignInPhone(final String countryCode, final String phone, final ErrorCallback errorCallback) { setAndStoreCountryCode(countryCode); setAndStorePhone(phone); zMessagingApi.requestPhoneConfirmationCode(countryCode + phone, KindOfAccess.LOGIN, new ZMessagingApi.PhoneConfirmationCodeRequestListener() { @Override public void onConfirmationCodeSent(KindOfAccess kindOfAccess) { setState(AppEntryState.PHONE_SET_CODE); } @Override public void onPasswordExists(KindOfAccess kindOfAccess) { } @Override public void onConfirmationCodeSendingFailed(KindOfAccess kindOfAccess, int errorCode, String message, String label) { if (AppEntryError.PHONE_INVALID_FORMAT.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_INVALID_FORMAT); // Recently sent SMS, take user to phoneVerificationCode page } else if (AppEntryError.PHONE_PENDING_LOGIN.correspondsTo(errorCode, label)) { setState(AppEntryState.PHONE_SET_CODE); // This phone number was never registered, redirect to registration } else if (AppEntryError.PHONE_INVALID.correspondsTo(errorCode, label)) { persistAppEntryPoint(AppEntryState.PHONE_REGISTER); setRegistrationPhone(countryCode, phone, errorCallback); // Pass error to UI } else if (AppEntryError.PHONE_BUDGET_EXHAUSTED.correspondsTo(errorCode, label)) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorSignIn()); errorCallback.onError(AppEntryError.PHONE_BUDGET_EXHAUSTED); } else { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorSignIn()); errorCallback.onError(AppEntryError.LOGIN_GENERIC_ERROR); } } }); } @Override public void triggerVerificationCodeCallToUser(final SuccessCallback successCallback, final ErrorCallback errorCallback) { final boolean isEntryPointPhoneRegister = entryPoint == AppEntryState.PHONE_REGISTER; boolean isEntryPointPhoneSignIn = entryPoint == AppEntryState.PHONE_SIGN_IN; KindOfAccess kindOfAccess = KindOfAccess.REGISTRATION; if (isEntryPointPhoneSignIn) { kindOfAccess = KindOfAccess.LOGIN; } zMessagingApi.requestPhoneConfirmationCall(countryCode + phone, kindOfAccess, new ZMessagingApi.PhoneConfirmationCodeRequestListener() { @Override public void onConfirmationCodeSent(KindOfAccess kindOfAccess) { if (isEntryPointPhoneRegister) { appEntryStateCallback.tagAppEntryEvent(new RequestedPhoneVerificationCallEvent(OutcomeAttribute.SUCCESS, "", getPhoneRegistrationContext())); } successCallback.onSuccess(); } @Override public void onPasswordExists(KindOfAccess kindOfAccess) { } @Override public void onConfirmationCodeSendingFailed(KindOfAccess kindOfAccess, int errorCode, String message, String label) { if (isEntryPointPhoneRegister) { appEntryStateCallback.tagAppEntryEvent(new RequestedPhoneVerificationCallEvent(OutcomeAttribute.FAIL, "", getPhoneRegistrationContext())); } if (AppEntryError.PHONE_BUDGET_EXHAUSTED.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_BUDGET_EXHAUSTED); } else { errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } @Override public void resendPhone(final SuccessCallback successCallback, final ErrorCallback errorCallback) { boolean isEntryPointPhoneSignIn = entryPoint == AppEntryState.PHONE_SIGN_IN; final boolean isEntryPointPhoneRegister = entryPoint == AppEntryState.PHONE_REGISTER; boolean isEntryPointEmailSignIn = entryPoint == AppEntryState.EMAIL_SIGN_IN; if (isEntryPointPhoneSignIn) { zMessagingApi.requestPhoneConfirmationCode(countryCode + phone, KindOfAccess.LOGIN, new ZMessagingApi.PhoneConfirmationCodeRequestListener() { @Override public void onConfirmationCodeSent(KindOfAccess kindOfAccess) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.resent(PhoneVerification.Context.SIGN_IN)); successCallback.onSuccess(); } @Override public void onPasswordExists(KindOfAccess kindOfAccess) { } @Override public void onConfirmationCodeSendingFailed(KindOfAccess kindOfAccess, int errorCode, String message, String label) { if (AppEntryError.PHONE_PENDING_LOGIN.correspondsTo( errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_PENDING_LOGIN); } else if (AppEntryError.PHONE_BUDGET_EXHAUSTED.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_BUDGET_EXHAUSTED); } else { errorCallback.onError(AppEntryError.LOGIN_GENERIC_ERROR); } } }); } else if (isEntryPointPhoneRegister || isEntryPointEmailSignIn) { zMessagingApi.requestPhoneConfirmationCode(countryCode + phone, KindOfAccess.REGISTRATION, new ZMessagingApi.PhoneConfirmationCodeRequestListener() { @Override public void onConfirmationCodeSent(KindOfAccess kindOfAccess) { if (isEntryPointPhoneRegister) { appEntryStateCallback.tagAppEntryEvent(new ResentPhoneVerificationEvent(OutcomeAttribute.SUCCESS, "", getPhoneRegistrationContext())); } else { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.resent(PhoneVerification.Context.POST_LOGIN)); } successCallback.onSuccess(); } @Override public void onPasswordExists(KindOfAccess kindOfAccess) { } @Override public void onConfirmationCodeSendingFailed(KindOfAccess kindOfAccess, int errorCode, String message, String label) { if (isEntryPointPhoneRegister) { appEntryStateCallback.tagAppEntryEvent(new ResentPhoneVerificationEvent(OutcomeAttribute.FAIL, String.valueOf(errorCode), registrationEventContext)); } else { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.codeRequestErrorSignIn()); } if (AppEntryError.PHONE_BUDGET_EXHAUSTED.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_BUDGET_EXHAUSTED); } else { errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } } @Override public void submitCode(String phoneVerificationCode, final ErrorCallback errorCallback) { setAndStorePhoneVerificationCode(phoneVerificationCode); if (entryPoint == AppEntryState.PHONE_REGISTER) { zMessagingApi.verifyPhoneNumber(countryCode + phone, phoneVerificationCode, KindOfVerification.PREVERIFY_ON_REGISTRATION, new ZMessagingApi.PhoneNumberVerificationListener() { @Override public void onVerified(KindOfVerification kindOfVerification) { appEntryStateCallback.tagAppEntryEvent(new VerifiedPhoneEvent( OutcomeAttribute.SUCCESS, "", "", getPhoneRegistrationContext())); setState(AppEntryState.PHONE_SET_NAME); } @Override public void onVerificationFailed(KindOfVerification kindOfVerification, int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(new VerifiedPhoneEvent( OutcomeAttribute.FAIL, String.valueOf(errorCode), message + "; " + label, getPhoneRegistrationContext())); if (AppEntryError.PHONE_INVALID_REGISTRATION_CODE.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_INVALID_REGISTRATION_CODE); } else { errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } else if (entryPoint == AppEntryState.PHONE_SIGN_IN) { ignoreSelfUpdates = true; zMessagingApi.login(CredentialsFactory.phoneCredentials(countryCode + phone, phoneVerificationCode), new LoginListener() { @Override public void onSuccess(Self self) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.success(PhoneVerification.Context.SIGN_IN)); appEntryStateCallback.tagAppEntryEvent(new LoggedInEvent(true)); bindSelf(self); setState(AppEntryState.PHONE_SIGNED_IN); ignoreSelfUpdates = false; } @Override public void onFailed(int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.error(PhoneVerification.Context.SIGN_IN)); if (AppEntryError.PHONE_INVALID_LOGIN_CODE.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.PHONE_INVALID_LOGIN_CODE); } else if (AppEntryError.TOO_MANY_ATTEMPTS.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.TOO_MANY_ATTEMPTS); } else { errorCallback.onError(AppEntryError.LOGIN_GENERIC_ERROR); } ignoreSelfUpdates = false; } }); } else if (entryPoint == AppEntryState.EMAIL_SIGN_IN) { zMessagingApi.verifyPhoneNumber(countryCode + phone, phoneVerificationCode, KindOfVerification.VERIFY_ON_UPDATE, new ZMessagingApi.PhoneNumberVerificationListener() { @Override public void onVerified(KindOfVerification kindOfVerification) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.success(PhoneVerification.Context.POST_LOGIN)); setState(AppEntryState.LOGGED_IN); } @Override public void onVerificationFailed(KindOfVerification kindOfVerification, int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(PhoneVerification.error(PhoneVerification.Context.POST_LOGIN)); if (AppEntryError.PHONE_INVALID_ADD_CODE.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_INVALID_ADD_CODE); } else { errorCallback.onError(AppEntryError.LOGIN_GENERIC_ERROR); } } }); } } @Override public void registerWithPhone(String name, AccentColor accentColor, final ErrorCallback errorCallback) { if (alreadyRegisteredWithPhone()) { self.setName(name); setState(AppEntryState.PHONE_SET_PICTURE); } else { zMessagingApi.register(CredentialsFactory.phoneCredentials(countryCode + phone, phoneVerificationCode), name, accentColor, new ZMessagingApi.RegistrationListener() { @Override public void onRegistered(Self self) { appEntryStateCallback.tagAppEntryEvent(new EnteredNameEvent(OutcomeAttribute.SUCCESS, "", getPhoneRegistrationContext())); appEntryStateCallback.tagAppEntryEvent(new SucceededWithRegistrationEvent(getPhoneRegistrationContext())); bindSelf(self); setState(AppEntryState.PHONE_SET_PICTURE); } @Override public void onRegistrationFailed(int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(new EnteredNameEvent(OutcomeAttribute.FAIL, String.valueOf(errorCode), getPhoneRegistrationContext())); if (AppEntryError.PHONE_INVALID_REGISTRATION_CODE.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_INVALID_REGISTRATION_CODE); } else if (AppEntryError.PHONE_EXISTS.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_EXISTS); } else { errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } setAndStoreName(name); } private boolean alreadyRegisteredWithPhone() { //this could be true if the user navigates back return self != null && self.isLoggedIn(); } @Override public void setPhonePicture(ImageAsset imageAsset) { zMessagingApi.getSelf().setPicture(imageAsset); setState(AppEntryState.LOGGED_IN); } @Override public void addEmailAndPasswordToPhone(final String email, final String password, final ErrorCallback emailErrorCallback, final ErrorCallback passwordErrorCallback) { setAndStoreEmail(email); this.password = password; final CredentialsUpdateListener emailUpdateListener = new CredentialsUpdateListener() { @Override public void onUpdated() { appEntryStateCallback.tagAppEntryEvent(EmailVerification.submitted(EmailVerification.Context.POST_LOGIN)); appEntryStateCallback.tagAppEntryEvent(EditSelfUser.emailAddedSignIn()); setState(AppEntryState.PHONE_VERIFY_EMAIL); } @Override public void onUpdateFailed(int errorCode, String message, String label) { if (AppEntryError.EMAIL_EXISTS.correspondsTo(errorCode, label)) { emailErrorCallback.onError(AppEntryError.EMAIL_EXISTS); } else if (AppEntryError.EMAIL_INVALID.correspondsTo(errorCode, label)) { emailErrorCallback.onError(AppEntryError.EMAIL_INVALID); } else { emailErrorCallback.onError(AppEntryError.EMAIL_GENERIC_ERROR); } } }; self.setPassword(password, new CredentialsUpdateListener() { @Override public void onUpdated() { appEntryStateCallback.tagAppEntryEvent(EditSelfUser.passwordAddedSignIn()); self.setEmail(email, emailUpdateListener); } @Override public void onUpdateFailed(int errorCode, String message, String label) { // Edge case where password was set on another device while email/pw // were being added on this one. if (errorCode == AppEntryError.FORBIDDEN.errorCode) { self.setEmail(email, emailUpdateListener); } else { passwordErrorCallback.onError(AppEntryError.PHONE_ADD_PASSWORD); } } }); } @Override public void setEmailPicture(ImageAsset imageAsset) { zMessagingApi.getSelf().setPicture(imageAsset); setState(AppEntryState.LOGGED_IN); } @Override public void registerWithEmail(final String email, final String password, final String name, final AccentColor accentColor, final ErrorCallback errorCallback) { setAndStoreEmail(email); setAndStoreName(name); this.password = password; zMessagingApi.register(CredentialsFactory.emailCredentials(email, password), name, accentColor, new ZMessagingApi.RegistrationListener() { @Override public void onRegistered(Self self) { appEntryStateCallback.tagAppEntryEvent(new EnteredEmailAndPasswordEvent(OutcomeAttribute.SUCCESS, "", getEmailRegistrationContext())); bindSelf(self); setState(AppEntryState.EMAIL_VERIFY_EMAIL); } @Override public void onRegistrationFailed(int errorCode, String message, String label) { appEntryStateCallback.tagAppEntryEvent(new EnteredEmailAndPasswordEvent(OutcomeAttribute.FAIL, String.valueOf(errorCode), getEmailRegistrationContext())); if (AppEntryError.EMAIL_EXISTS.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.EMAIL_EXISTS); } else if (AppEntryError.SERVER_CONNECTIVITY_ERROR.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.SERVER_CONNECTIVITY_ERROR); } else { errorCallback.onError(AppEntryError.EMAIL_REGISTER_GENERIC_ERROR); } } }); } @Override public void acceptEmailInvitation(String password, AccentColor accentColor) { Credentials credentials = CredentialsFactory.emailInvitationCredentials(invitationEmail, password, invitationToken); zMessagingApi.register(credentials, invitationName, accentColor, new ZMessagingApi.RegistrationListener() { @Override public void onRegistered(Self self) { bindSelf(self); setState(AppEntryState.EMAIL_SET_PICTURE); appEntryStateCallback.onInvitationSuccess(); appEntryStateCallback.tagAppEntryEvent(new SucceededWithRegistrationEvent(RegistrationEventContext.PERSONAL_INVITE_EMAIL)); } @Override public void onRegistrationFailed(int code, String message, String label) { Timber.e("Email invitation registration failed"); appEntryStateCallback.onInvitationFailed(); if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } } }); } @Override public void acceptPhoneInvitation(AccentColor accentColor) { Credentials credentials = CredentialsFactory.phoneInvitationCredentials(invitationPhone, invitationToken); zMessagingApi.register(credentials, invitationName, accentColor, new ZMessagingApi.RegistrationListener() { @Override public void onRegistered(Self self) { bindSelf(self); setState(AppEntryState.PHONE_SET_PICTURE); appEntryStateCallback.onInvitationSuccess(); appEntryStateCallback.tagAppEntryEvent(new SucceededWithRegistrationEvent(RegistrationEventContext.PERSONAL_INVITE_PHONE)); } @Override public void onRegistrationFailed(int i, String s, String s1) { Timber.e("Email invitation registration failed"); appEntryStateCallback.onInvitationFailed(); if (LayoutSpec.isPhone(context)) { setState(AppEntryState.PHONE_REGISTER); } else { setState(AppEntryState.EMAIL_WELCOME); } } }); } @Override public void signInWithEmail(String email, String password, final ErrorCallback errorCallback) { setAndStoreEmail(email); this.password = password; ignoreSelfUpdates = true; zMessagingApi.login(CredentialsFactory.emailCredentials(email, password), new LoginListener() { @Override public void onSuccess(Self self) { bindSelf(self); setState(AppEntryState.EMAIL_SIGNED_IN); ignoreSelfUpdates = false; appEntryStateCallback.tagAppEntryEvent(new LoggedInEvent(false)); } @Override public void onFailed(int errorCode, String message, String label) { if (AppEntryError.EMAIL_INVALID_REQUEST.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.EMAIL_INVALID_REQUEST); } else if (AppEntryError.SERVER_CONNECTIVITY_ERROR.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.SERVER_CONNECTIVITY_ERROR); } else if (AppEntryError.EMAIL_INVALID_LOGIN_CREDENTIALS.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.EMAIL_INVALID_LOGIN_CREDENTIALS); } else if (AppEntryError.TOO_MANY_ATTEMPTS.correspondsTo(errorCode, "")) { errorCallback.onError(AppEntryError.TOO_MANY_ATTEMPTS); } else if (AppEntryError.NO_INTERNET.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.NO_INTERNET); } else { errorCallback.onError(AppEntryError.LOGIN_GENERIC_ERROR); } ignoreSelfUpdates = false; } }); } @Override public void addPhoneToEmail(String countryCode, String phone, final ErrorCallback errorCallback) { setAndStoreCountryCode(countryCode); setAndStorePhone(phone); self.setPhone(countryCode + phone, new CredentialsUpdateListener() { @Override public void onUpdated() { appEntryStateCallback.tagAppEntryEvent(EditSelfUser.phoneAddedSignIn()); setState(AppEntryState.EMAIL_SET_CODE); } @Override public void onUpdateFailed(int errorCode, String message, String label) { if (AppEntryError.PHONE_EXISTS.correspondsTo(errorCode, label)) { errorCallback.onError(AppEntryError.PHONE_EXISTS); } else { errorCallback.onError(AppEntryError.PHONE_REGISTER_GENERIC_ERROR); } } }); } @Override public void resendEmail() { switch (entryPoint) { case EMAIL_REGISTER: appEntryStateCallback.tagAppEntryEvent(new ResentEmailVerificationEvent(OutcomeAttribute.SUCCESS, "", getEmailRegistrationContext())); break; case PHONE_SIGN_IN: appEntryStateCallback.tagAppEntryEvent(EmailVerification.resent(EmailVerification.Context.POST_LOGIN)); break; } zMessagingApi.getSelf().resendVerificationEmail(email); } private void setAndStorePhone(String phone) { context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().putString(PREF_ITEM_PHONE, phone).apply(); this.phone = phone; } private void setAndStoreCountryCode(String countryCode) { context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().putString(PREF_ITEM_COUNTRY_CODE, countryCode).apply(); this.countryCode = countryCode; } private void setAndStorePhoneVerificationCode(String phoneVerificationCode) { context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().putString(PREF_ITEM_PHONE_VERIFICATION_CODE, countryCode).apply(); this.phoneVerificationCode = phoneVerificationCode; } private void setAndStoreName(String name) { context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().putString(PREF_ITEM_NAME, name).apply(); this.name = name; } private void setAndStoreEmail(String email) { context.getSharedPreferences(PREF_REGISTRATION, Context.MODE_PRIVATE).edit().putString(PREF_ITEM_EMAIL, email).apply(); this.email = email; } //////////////////////// // // Getters from UI // //////////////////////// @Override public String getPhone() { return phone; } @Override public String getEmail() { return email; } @Override public String getPassword() { return password; } @Override public String getName() { return name; } @Override public String getUserId() { return self != null && self.getUser() != null ? self.getUser().getId() : null; } @Override public String getInvitationEmail() { return invitationEmail; } @Override public String getInvitationPhone() { return invitationPhone; } @Override public String getInvitationName() { return invitationName; } @Override public Invitations.PersonalToken getInvitationToken() { return invitationToken; } @Override public void setRegistrationContext(RegistrationEventContext registrationEventContext) { this.registrationEventContext = registrationEventContext; switch (registrationEventContext) { case PHONE: case GENERIC_INVITE_PHONE: case PERSONAL_INVITE_PHONE: appEntryStateCallback.tagAppEntryEvent(new OpenedPhoneSignUpEvent(registrationEventContext)); break; case EMAIL: case GENERIC_INVITE_EMAIL: case PERSONAL_INVITE_EMAIL: appEntryStateCallback.tagAppEntryEvent(new OpenedEmailSignUpEvent(registrationEventContext)); break; } } @Override public RegistrationEventContext getPhoneRegistrationContext() { return registrationEventContext == null ? RegistrationEventContext.PHONE : registrationEventContext; } @Override public RegistrationEventContext getEmailRegistrationContext() { return registrationEventContext == null ? RegistrationEventContext.EMAIL : registrationEventContext; } @Override public String getCountryCode() { return countryCode; } @Override public void onError(ErrorsList.ErrorDescription errorDescription) { Timber.i("onError id=%s, conv=%s, resp=%s, time=%s, type=%s", errorDescription.getId(), errorDescription.getConversation(), errorDescription.getResponse(), errorDescription.getTime(), errorDescription.getType()); } }