package co.smartreceipts.android.identity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.common.base.Preconditions;
import org.reactivestreams.Subscriber;
import javax.inject.Inject;
import co.smartreceipts.android.analytics.Analytics;
import co.smartreceipts.android.analytics.events.ErrorEvent;
import co.smartreceipts.android.analytics.events.Events;
import co.smartreceipts.android.apis.ApiValidationException;
import co.smartreceipts.android.apis.hosts.ServiceManager;
import co.smartreceipts.android.di.scopes.ApplicationScope;
import co.smartreceipts.android.identity.apis.login.LoginPayload;
import co.smartreceipts.android.identity.apis.login.LoginResponse;
import co.smartreceipts.android.identity.apis.login.LoginService;
import co.smartreceipts.android.identity.apis.login.LoginType;
import co.smartreceipts.android.identity.apis.login.UserCredentialsPayload;
import co.smartreceipts.android.identity.apis.logout.LogoutResponse;
import co.smartreceipts.android.identity.apis.logout.LogoutService;
import co.smartreceipts.android.identity.apis.me.MeResponse;
import co.smartreceipts.android.identity.apis.me.MeService;
import co.smartreceipts.android.identity.apis.organizations.OrganizationsResponse;
import co.smartreceipts.android.identity.apis.signup.SignUpPayload;
import co.smartreceipts.android.identity.apis.signup.SignUpService;
import co.smartreceipts.android.identity.store.EmailAddress;
import co.smartreceipts.android.identity.store.IdentityStore;
import co.smartreceipts.android.identity.store.MutableIdentityStore;
import co.smartreceipts.android.identity.store.Token;
import co.smartreceipts.android.push.apis.me.UpdatePushTokensRequest;
import co.smartreceipts.android.settings.UserPreferenceManager;
import co.smartreceipts.android.utils.log.Logger;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
@ApplicationScope
public class IdentityManager implements IdentityStore {
private final ServiceManager serviceManager;
private final Analytics analytics;
private final MutableIdentityStore mutableIdentityStore;
private final OrganizationManager organizationManager;
private final BehaviorSubject<Boolean> isLoggedInBehaviorSubject;
@Inject
public IdentityManager(Analytics analytics,
UserPreferenceManager userPreferenceManager,
MutableIdentityStore mutableIdentityStore,
ServiceManager serviceManager) {
this.serviceManager = serviceManager;
this.analytics = analytics;
this.mutableIdentityStore = mutableIdentityStore;
this.organizationManager = new OrganizationManager(serviceManager, mutableIdentityStore, userPreferenceManager);
this.isLoggedInBehaviorSubject = BehaviorSubject.createDefault(isLoggedIn());
}
@Nullable
@Override
public EmailAddress getEmail() {
return mutableIdentityStore.getEmail();
}
@Nullable
@Override
public Token getToken() {
return mutableIdentityStore.getToken();
}
@Override
public boolean isLoggedIn() {
return mutableIdentityStore.isLoggedIn();
}
/**
* @return an {@link Observable} relay that will only emit {@link Subscriber#onNext(Object)} calls
* (and never {@link Subscriber#onComplete()} or {@link Subscriber#onError(Throwable)} calls) under
* the following circumstances:
* <ul>
* <li>When the app launches, it will emit {@code true} if logged in and {@code false} if not</li>
* <li>When the user signs in, it will emit {@code true}</li>
* <li>When the user signs out, it will emit {@code false}</li>
* </ul>
* <p>
* Users of this class should expect a {@link BehaviorSubject} type behavior in which the current
* state will always be emitted as soon as we subscribe
* </p>
*/
@NonNull
public Observable<Boolean> isLoggedInStream() {
return isLoggedInBehaviorSubject;
}
public synchronized Observable<LoginResponse> logInOrSignUp(@NonNull final UserCredentialsPayload credentials) {
Preconditions.checkNotNull(credentials.getEmail(), "A valid email must be provided to log-in");
final Observable<LoginResponse> loginResponseObservable;
if (credentials.getLoginType() == LoginType.LogIn) {
Logger.info(this, "Initiating user log in");
this.analytics.record(Events.Identity.UserLogin);
loginResponseObservable = serviceManager.getService(LoginService.class).logIn(new LoginPayload(credentials));
} else if (credentials.getLoginType() == LoginType.SignUp) {
Logger.info(this, "Initiating user sign up");
this.analytics.record(Events.Identity.UserSignUp);
loginResponseObservable = serviceManager.getService(SignUpService.class).signUp(new SignUpPayload(credentials));
} else {
throw new IllegalArgumentException("Unsupported log in type");
}
return loginResponseObservable
.flatMap(loginResponse -> {
if (loginResponse.getToken() != null) {
mutableIdentityStore.setEmailAndToken(credentials.getEmail(), loginResponse.getToken());
return Observable.just(loginResponse);
} else {
return Observable.error(new ApiValidationException("The response did not contain a valid API token"));
}
})
.flatMap(loginResponse -> organizationManager.getOrganizations()
.flatMap(response -> Observable.just(loginResponse)))
.doOnError(throwable -> {
if (credentials.getLoginType() == LoginType.LogIn) {
Logger.error(this, "Failed to complete the log in request", throwable);
analytics.record(Events.Identity.UserLoginFailure);
} else if (credentials.getLoginType() == LoginType.SignUp) {
Logger.error(this, "Failed to complete the sign up request", throwable);
analytics.record(Events.Identity.UserSignUpFailure);
}
analytics.record(new ErrorEvent(IdentityManager.this, throwable));
})
.doOnComplete(() -> {
isLoggedInBehaviorSubject.onNext(true);
if (credentials.getLoginType() == LoginType.LogIn) {
Logger.info(this, "Successfully completed the log in request");
analytics.record(Events.Identity.UserLoginSuccess);
} else if (credentials.getLoginType() == LoginType.SignUp) {
Logger.info(this, "Successfully completed the sign up request");
analytics.record(Events.Identity.UserSignUpSuccess);
}
});
}
public synchronized Observable<LogoutResponse> logOut() {
Logger.info(this, "Initiating user log-out");
this.analytics.record(Events.Identity.UserLogout);
return serviceManager.getService(LogoutService.class).logOut()
.doOnNext(logoutResponse -> mutableIdentityStore.setEmailAndToken(null, null))
.doOnError(throwable -> {
Logger.error(this, "Failed to complete the log-out request", throwable);
analytics.record(Events.Identity.UserLogoutFailure);
})
.doOnComplete(() -> {
Logger.info(this, "Successfully completed the log-out request");
isLoggedInBehaviorSubject.onNext(false);
analytics.record(Events.Identity.UserLogoutSuccess);
});
}
@NonNull
public Observable<MeResponse> getMe() {
if (isLoggedIn()) {
return serviceManager.getService(MeService.class).me();
} else {
return Observable.error(new IllegalStateException("Cannot fetch the user's account until we're logged in"));
}
}
@NonNull
public Observable<MeResponse> updateMe(@NonNull UpdatePushTokensRequest request) {
if (isLoggedIn()) {
return serviceManager.getService(MeService.class).me(request);
} else {
return Observable.error(new IllegalStateException("Cannot fetch the user's account until we're logged in"));
}
}
@NonNull
public Observable<OrganizationsResponse> getOrganizations() {
return organizationManager.getOrganizations();
}
}