diff --git a/app/src/main/java/com/kickstarter/libs/CurrentUser.java b/app/src/main/java/com/kickstarter/libs/CurrentUser.java index 12ba466c92..caec0f50d5 100644 --- a/app/src/main/java/com/kickstarter/libs/CurrentUser.java +++ b/app/src/main/java/com/kickstarter/libs/CurrentUser.java @@ -49,11 +49,19 @@ public String getAccessToken() { } @Override - public void login(final @NonNull User newUser, final @NonNull String accessToken) { + public void login(final @NonNull User newUser) { Timber.d("Login user %s", newUser.name()); + this.user.onNext(newUser); + } + @Override + public void setToken(final @NonNull String accessToken) { + // - Clean previous token in case there is any + this.accessTokenPreference.delete(); + this.deviceRegistrar.unregisterDevice(); + + // - Register new token this.accessTokenPreference.set(accessToken); - this.user.onNext(newUser); this.deviceRegistrar.registerDevice(); } diff --git a/app/src/main/java/com/kickstarter/libs/CurrentUserType.java b/app/src/main/java/com/kickstarter/libs/CurrentUserType.java index 8c31d569c9..deb40dbcdd 100644 --- a/app/src/main/java/com/kickstarter/libs/CurrentUserType.java +++ b/app/src/main/java/com/kickstarter/libs/CurrentUserType.java @@ -9,11 +9,19 @@ public abstract class CurrentUserType { + /*** + * Persist a new token, + * - retrieved form #exchange endpoint {/v1/oauth/authorizations/exchange} + * - retrieved from Facebook login + * - retrieved from endpoint {/xauth/access_token} soon to be deprecated + */ + public abstract void setToken(final @NonNull String accessToken); + /** * Call when a user has logged in. The implementation of `CurrentUserType` is responsible - * for persisting the user and access token. + * for persisting the user. */ - public abstract void login(final @NonNull User newUser, final @NonNull String accessToken); + public abstract void login(final @NonNull User newUser); /** * Call when a user should be logged out. diff --git a/app/src/main/java/com/kickstarter/libs/CurrentUserV2.kt b/app/src/main/java/com/kickstarter/libs/CurrentUserV2.kt index 9c62477323..7a3e949915 100644 --- a/app/src/main/java/com/kickstarter/libs/CurrentUserV2.kt +++ b/app/src/main/java/com/kickstarter/libs/CurrentUserV2.kt @@ -10,11 +10,17 @@ import io.reactivex.subjects.BehaviorSubject import timber.log.Timber abstract class CurrentUserTypeV2 { + + /*** + * Persist a new token, retrieved form #exchange endpoint {/v1/oauth/authorizations/exchange} + */ + abstract fun setToken(accessToken: String) + /** * Call when a user has logged in. The implementation of `CurrentUserType` is responsible - * for persisting the user and access token. + * for persisting the user. */ - abstract fun login(newUser: User, accessToken: String) + abstract fun login(newUser: User) /** * Call when a user should be logged out. @@ -106,10 +112,18 @@ class CurrentUserV2( override val accessToken: String? get() = accessTokenPreference.get() - override fun login(newUser: User, accessToken: String) { + override fun login(newUser: User) { Timber.d("Login user %s", newUser.name()) - accessTokenPreference.set(accessToken) user.onNext(KsOptional.of(newUser)) + } + + override fun setToken(accessToken: String) { + // - Clean previous token in case there is any + accessTokenPreference.delete() + deviceRegistrar.unregisterDevice() + + // - Register new token + accessTokenPreference.set(accessToken) deviceRegistrar.registerDevice() } diff --git a/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.java b/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.java deleted file mode 100644 index bffa596e99..0000000000 --- a/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.kickstarter.libs.utils; - -import android.app.Activity; -import android.app.Application; -import android.content.ComponentCallbacks2; -import android.content.res.Configuration; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.facebook.appevents.AppEventsLogger; -import com.google.firebase.analytics.FirebaseAnalytics; -import com.kickstarter.KSApplication; -import com.kickstarter.libs.Build; -import com.kickstarter.libs.CurrentConfigType; -import com.kickstarter.libs.CurrentUserType; -import com.kickstarter.libs.Logout; -import com.kickstarter.libs.preferences.StringPreferenceType; -import com.kickstarter.libs.rx.transformers.Transformers; -import com.kickstarter.libs.utils.extensions.AnyExtKt; -import com.kickstarter.libs.utils.extensions.ConfigExtension; -import com.kickstarter.services.ApiClientType; -import com.kickstarter.services.apiresponses.ErrorEnvelope; - -import javax.inject.Inject; - -public final class ApplicationLifecycleUtil implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { - protected @Inject ApiClientType client; - protected @Inject CurrentConfigType config; - protected @Inject CurrentUserType currentUser; - protected @Inject Logout logout; - protected @Inject Build build; - protected @Inject StringPreferenceType featuresFlagPreference; - - private final KSApplication application; - private boolean isInBackground = true; - private boolean isLoggedIn; - - public ApplicationLifecycleUtil(final @NonNull KSApplication application) { - this.application = application; - application.component().inject(this); - - this.currentUser.isLoggedIn().subscribe(userLoggedIn -> { - this.isLoggedIn = userLoggedIn; - }); - } - - @Override - public void onActivityCreated(final @NonNull Activity activity, final @Nullable Bundle bundle) { - } - - @Override - public void onActivityStarted(final @NonNull Activity activity) { - } - - @Override - public void onActivityResumed(final @NonNull Activity activity) { - if(this.isInBackground) { - // Facebook: logs 'install' and 'app activate' App Events. - AppEventsLogger.activateApp(activity.getApplication()); - - refreshConfigFile(); - refreshUser(); - - this.isInBackground = false; - } - } - - /** - * Refresh the config file. - */ - private void refreshConfigFile() { - this.client.config() - .materialize() - .share() - .subscribe(notification -> { - if (notification.hasValue()) { - //sync save features flags in the config object - if (this.build.isDebug() || Build.isInternal()) { - ConfigExtension.syncUserFeatureFlagsFromPref(notification.getValue(), this.featuresFlagPreference); - } - this.config.config(notification.getValue()); - } - if (notification.hasThrowable()) { - this.handleConfigApiError(ErrorEnvelope.fromThrowable(notification.getThrowable())); - } - }); - } - - /** - * Handles a config API error by logging the user out in the case of a 401. We will interpret - * 401's on the config request as meaning the user's current access token is no longer valid, - * as that endpoint should never 401 othewise. - */ - private void handleConfigApiError(final @NonNull ErrorEnvelope error) { - if (error.httpCode() == 401) { - forceLogout("config_api_error"); - } - } - - /** - * Forces the current user session to be logged out. - */ - private void forceLogout(final @NonNull String context) { - this.logout.execute(); - ApplicationUtils.startNewDiscoveryActivity(this.application); - final Bundle params = new Bundle(); - params.putString("location", context); - - FirebaseAnalytics.getInstance(this.application).logEvent("force_logout", params); - } - - /** - * Refreshes the user object if there is not a user logged in with a non-null access token. - */ - private void refreshUser() { - final String accessToken = this.currentUser.getAccessToken(); - - // Check if the access token is null and the user is still logged in. - if (this.isLoggedIn && AnyExtKt.isNull(accessToken)) { - forceLogout("access_token_null"); - } else { - if (AnyExtKt.isNotNull(accessToken)) { - this.client.fetchCurrentUser() - .compose(Transformers.neverError()) - .subscribe(u -> this.currentUser.refresh(u)); - } - } - } - - @Override - public void onActivityPaused(final @NonNull Activity activity) { - } - - @Override - public void onActivityStopped(final @NonNull Activity activity) { - } - - @Override - public void onActivitySaveInstanceState(final @NonNull Activity activity, final @Nullable Bundle bundle) { - } - - @Override - public void onActivityDestroyed(final @NonNull Activity activity) { - } - - @Override - public void onConfigurationChanged(final @NonNull Configuration configuration) { - } - - @Override - public void onLowMemory() { - } - - /** - * Memory availability callback. TRIM_MEMORY_UI_HIDDEN means the app's UI is no longer visible. - * This is triggered when the user navigates out of the app and primarily used to free resources used by the UI. - * http://developer.android.com/training/articles/memory.html - */ - @Override - public void onTrimMemory(final int i) { - if(i == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { - this.isInBackground = true; - } - } -} diff --git a/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.kt b/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.kt new file mode 100644 index 0000000000..6b10f4003e --- /dev/null +++ b/app/src/main/java/com/kickstarter/libs/utils/ApplicationLifecycleUtil.kt @@ -0,0 +1,151 @@ +package com.kickstarter.libs.utils + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.content.ComponentCallbacks2 +import android.content.res.Configuration +import android.os.Bundle +import com.facebook.appevents.AppEventsLogger +import com.google.firebase.analytics.FirebaseAnalytics +import com.kickstarter.KSApplication +import com.kickstarter.libs.Build +import com.kickstarter.libs.CurrentConfigTypeV2 +import com.kickstarter.libs.CurrentUserTypeV2 +import com.kickstarter.libs.Logout +import com.kickstarter.libs.preferences.StringPreferenceType +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.extensions.addToDisposable +import com.kickstarter.libs.utils.extensions.isNotNull +import com.kickstarter.libs.utils.extensions.isNull +import com.kickstarter.libs.utils.extensions.syncUserFeatureFlagsFromPref +import com.kickstarter.services.ApiClientTypeV2 +import com.kickstarter.services.apiresponses.ErrorEnvelope +import com.kickstarter.services.apiresponses.ErrorEnvelope.Companion.fromThrowable +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +class ApplicationLifecycleUtil(private val application: KSApplication) : + ActivityLifecycleCallbacks, + ComponentCallbacks2 { + + @Inject + lateinit var client: ApiClientTypeV2 + + @Inject + lateinit var config: CurrentConfigTypeV2 + + @Inject + lateinit var currentUser: CurrentUserTypeV2 + + @Inject + lateinit var logout: Logout + + @Inject + lateinit var build: Build + + @JvmField + @Inject + var featuresFlagPreference: StringPreferenceType? = null + private var isInBackground = true + private var isLoggedIn = false + private val disposables = CompositeDisposable() + + init { + application.component().inject(this) + currentUser.observable().filter { it.isPresent() }.subscribe { isLoggedIn = true }.addToDisposable(disposables) + } + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) { + if (isInBackground) { + // Facebook: logs 'install' and 'app activate' App Events. + AppEventsLogger.activateApp(activity.application) + refreshConfigFile() + refreshUser() + isInBackground = false + } + } + + /** + * Refresh the config file. + */ + private fun refreshConfigFile() { + client.config() + .materialize() + .share() + .subscribe { notification -> + notification.value?.let { + // sync save features flags in the config object + if (build.isDebug || Build.isInternal()) { + it.syncUserFeatureFlagsFromPref(featuresFlagPreference!!) + } + config.config(it) + } + + notification.error?.let { + handleConfigApiError(fromThrowable(it)) + } + }.addToDisposable(disposables) + } + + /** + * Handles a config API error by logging the user out in the case of a 401. We will interpret + * 401's on the config request as meaning the user's current access token is no longer valid, + * as that endpoint should never 401 othewise. + */ + private fun handleConfigApiError(error: ErrorEnvelope) { + if (error.httpCode() == 401) { + forceLogout("config_api_error") + } + } + + /** + * Forces the current user session to be logged out. + */ + private fun forceLogout(context: String) { + logout.execute() + ApplicationUtils.startNewDiscoveryActivity(application) + val params = Bundle() + params.putString("location", context) + FirebaseAnalytics.getInstance(application).logEvent("force_logout", params) + } + + /** + * Refreshes the user object if there is not a user logged in with a non-null access token. + */ + private fun refreshUser() { + val accessToken = currentUser.accessToken ?: "" + + // Check if the access token is null and the user is still logged in. + if (isLoggedIn && accessToken.isNull()) { + forceLogout("access_token_null") + } else { + if (accessToken.isNotNull() && accessToken.isNotEmpty()) { + client.fetchCurrentUser() + .compose(Transformers.neverErrorV2()) + .subscribe { user -> + currentUser.refresh(user) + }.addToDisposable(disposables) + } + } + } + + override fun onActivityPaused(activity: Activity) { disposables.clear() } + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + override fun onConfigurationChanged(configuration: Configuration) {} + override fun onLowMemory() {} + + /** + * Memory availability callback. TRIM_MEMORY_UI_HIDDEN means the app's UI is no longer visible. + * This is triggered when the user navigates out of the app and primarily used to free resources used by the UI. + * http://developer.android.com/training/articles/memory.html + */ + override fun onTrimMemory(i: Int) { + if (i == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + isInBackground = true + } + } +} diff --git a/app/src/main/java/com/kickstarter/mock/services/MockApiClientV2.kt b/app/src/main/java/com/kickstarter/mock/services/MockApiClientV2.kt index 616c670386..695138fcc4 100644 --- a/app/src/main/java/com/kickstarter/mock/services/MockApiClientV2.kt +++ b/app/src/main/java/com/kickstarter/mock/services/MockApiClientV2.kt @@ -288,10 +288,6 @@ open class MockApiClientV2 : ApiClientTypeV2 { return Observable.empty() } - override fun fetchCurrentUser(token: String): Observable { - return Observable.empty() - } - override fun fetchLocation(param: String): Observable { return Observable.just(sydney()) } diff --git a/app/src/main/java/com/kickstarter/services/ApiClientTypeV2.java b/app/src/main/java/com/kickstarter/services/ApiClientTypeV2.java index 3006db0da1..af4d28a74f 100644 --- a/app/src/main/java/com/kickstarter/services/ApiClientTypeV2.java +++ b/app/src/main/java/com/kickstarter/services/ApiClientTypeV2.java @@ -53,8 +53,6 @@ public interface ApiClientTypeV2 { @NonNull Observable fetchCurrentUser(); - @NonNull Observable fetchCurrentUser(final @NonNull String token); - @NonNull Observable fetchLocation(final @NonNull String param); @NonNull Observable> fetchProjectNotifications(); diff --git a/app/src/main/java/com/kickstarter/services/ApiClientV2.java b/app/src/main/java/com/kickstarter/services/ApiClientV2.java index 04345d9773..5da7a6dd90 100644 --- a/app/src/main/java/com/kickstarter/services/ApiClientV2.java +++ b/app/src/main/java/com/kickstarter/services/ApiClientV2.java @@ -134,14 +134,6 @@ public ApiClientV2(final @NonNull ApiServiceV2 service, final @NonNull Gson gson .subscribeOn(Schedulers.io()); } - @Override - public @NonNull Observable fetchCurrentUser(final @NonNull String token) { - return this.service - .currentUser(token) - .lift(apiErrorOperator()) - .subscribeOn(Schedulers.io()); - } - @Override public @NonNull Observable fetchLocation(final @NonNull String param) { return this.service.location(param) diff --git a/app/src/main/java/com/kickstarter/services/interceptors/ApiRequestInterceptor.kt b/app/src/main/java/com/kickstarter/services/interceptors/ApiRequestInterceptor.kt index 92025b19a5..5d2764ecea 100644 --- a/app/src/main/java/com/kickstarter/services/interceptors/ApiRequestInterceptor.kt +++ b/app/src/main/java/com/kickstarter/services/interceptors/ApiRequestInterceptor.kt @@ -38,6 +38,10 @@ class ApiRequestInterceptor( .addHeader("Kickstarter-Android-App-UUID", FirebaseHelper.identifier) .addHeader("User-Agent", userAgent(build)) + this.currentUser.accessToken?.let { token -> + if (token.isNotEmpty()) builder.addHeader("X-Auth", "token $token") + } + return builder .url(url(initialRequest.url)) .build() @@ -46,13 +50,6 @@ class ApiRequestInterceptor( private fun url(initialHttpUrl: HttpUrl): HttpUrl { val builder: Builder = initialHttpUrl.newBuilder() .setQueryParameter("client_id", clientId) - currentUser.observable() - .filter { it.isPresent() } - .subscribe { - if (currentUser.accessToken != null) { - builder.setQueryParameter("oauth_token", currentUser.accessToken) - } - }.dispose() return builder.build() } diff --git a/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java b/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java index da38ba4b73..124c96d2a5 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java +++ b/app/src/main/java/com/kickstarter/viewmodels/FacebookConfirmationViewModel.java @@ -104,7 +104,8 @@ public ViewModel(final @NonNull Environment environment) { } private void registerWithFacebookSuccess(final @NonNull AccessTokenEnvelope envelope) { - this.loginUserCase.login(envelope.user(), envelope.accessToken()); + this.loginUserCase.setToken(envelope.accessToken()); + this.loginUserCase.setUser(envelope.user()); this.signupSuccess.onNext(null); } diff --git a/app/src/main/java/com/kickstarter/viewmodels/LoginToutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/LoginToutViewModel.kt index 7c5faba348..c9829c71b6 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/LoginToutViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/LoginToutViewModel.kt @@ -286,7 +286,7 @@ interface LoginToutViewModel { .filter { it.isNotNull() } .switchMap { this.loginUserCase - .loginAndUpdateUserPrivacyV2(it.user(), it.accessToken()) + .loginAndUpdateUserPrivacy(it.user(), it.accessToken()) } .subscribe { refreshUserUseCase.refresh(it) diff --git a/app/src/main/java/com/kickstarter/viewmodels/LoginViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/LoginViewModel.kt index e7f72cc4d3..9ec93f4b91 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/LoginViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/LoginViewModel.kt @@ -193,7 +193,7 @@ interface LoginViewModel { .distinctUntilChanged() .switchMap { this.loginUserCase - .loginAndUpdateUserPrivacyV2(it.user(), it.accessToken()) + .loginAndUpdateUserPrivacy(it.user(), it.accessToken()) } .subscribe { user -> this.success(user) diff --git a/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt index cb8111545f..31c09a8cea 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/OAuthViewModel.kt @@ -5,11 +5,11 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.kickstarter.libs.ApiEndpoint import com.kickstarter.libs.Environment import com.kickstarter.libs.utils.CodeVerifier import com.kickstarter.libs.utils.PKCE import com.kickstarter.libs.utils.Secrets +import com.kickstarter.libs.utils.Secrets.WebEndpoint import com.kickstarter.models.User import com.kickstarter.services.ApiException import com.kickstarter.viewmodels.usecases.LoginUseCase @@ -47,7 +47,11 @@ class OAuthViewModel( private val hostEndpoint = environment.webEndpoint() private val loginUseCase = LoginUseCase(environment) private val apiClient = requireNotNull(environment.apiClientV2()) - private val clientID = if (hostEndpoint == ApiEndpoint.PRODUCTION.name) Secrets.Api.Client.PRODUCTION else Secrets.Api.Client.STAGING + private val clientID = when (hostEndpoint) { + WebEndpoint.PRODUCTION -> Secrets.Api.Client.PRODUCTION + WebEndpoint.STAGING -> Secrets.Api.Client.STAGING + else -> "" + } private val codeVerifier = verifier.generateRandomCodeVerifier(entropy = CodeVerifier.MIN_CODE_VERIFIER_ENTROPY) private var mutableUIState = MutableStateFlow(OAuthUiState()) @@ -73,11 +77,12 @@ class OAuthViewModel( apiClient.loginWithCodes(codeVerifier, code, clientID) .asFlow() .flatMapLatest { token -> - Timber.d("retrieve user with token: $token") - apiClient.fetchCurrentUser(token.accessToken()) + Timber.d("About to persist token to currentUser: $token") + loginUseCase.setToken(token.accessToken()) + apiClient.fetchCurrentUser() .asFlow() .map { - Pair(token, it) + it } } .catch { @@ -90,12 +95,12 @@ class OAuthViewModel( ) loginUseCase.logout() } - .collect { - Timber.d("About to persist user and token to currentUser: $it") - loginUseCase.login(it.second, it.first.accessToken()) + .collect { user -> + Timber.d("About to persist user to currentUser: $user") + loginUseCase.setUser(user) mutableUIState.emit( OAuthUiState( - user = it.second, + user = user, ) ) } diff --git a/app/src/main/java/com/kickstarter/viewmodels/SetPasswordViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/SetPasswordViewModel.kt index f5514d9cb2..76098ad35f 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/SetPasswordViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/SetPasswordViewModel.kt @@ -123,9 +123,9 @@ interface SetPasswordViewModel { .distinctUntilChanged() .subscribe { currentUserV2.accessToken?.let { accessToken -> - loginUserCase.login( - it.first.toBuilder().needsPassword(false).build(), - accessToken + loginUserCase.setToken(accessToken) + loginUserCase.setUser( + it.first.toBuilder().needsPassword(false).build() ) } this.success.onNext(it.second.updateUserAccount()?.user()?.email() ?: "") diff --git a/app/src/main/java/com/kickstarter/viewmodels/SignupViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/SignupViewModel.kt index 92ee394a28..9eceabfb1a 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/SignupViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/SignupViewModel.kt @@ -125,7 +125,7 @@ interface SignupViewModel { .distinctUntilChanged() .switchMap { this.loginUserCase - .loginAndUpdateUserPrivacyV2(it.user(), it.accessToken()) + .loginAndUpdateUserPrivacy(it.user(), it.accessToken()) } .subscribe { success(it) } .addToDisposable(disposables) diff --git a/app/src/main/java/com/kickstarter/viewmodels/TwoFactorViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/TwoFactorViewModel.kt index 7b46a1a1c2..fc6f6de519 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/TwoFactorViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/TwoFactorViewModel.kt @@ -212,7 +212,7 @@ interface TwoFactorViewModel { } .switchMap { this.loginUserCase - .loginAndUpdateUserPrivacyV2(it.user(), it.accessToken()) + .loginAndUpdateUserPrivacy(it.user(), it.accessToken()) } .subscribe { success(it) } .addToDisposable(disposables) @@ -229,7 +229,7 @@ interface TwoFactorViewModel { } .switchMap { this.loginUserCase - .loginAndUpdateUserPrivacyV2(it.user(), it.accessToken()) + .loginAndUpdateUserPrivacy(it.user(), it.accessToken()) } .subscribe { success(it) } .addToDisposable(disposables) diff --git a/app/src/main/java/com/kickstarter/viewmodels/usecases/LoginUseCase.kt b/app/src/main/java/com/kickstarter/viewmodels/usecases/LoginUseCase.kt index cccc5eec18..cd486be717 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/usecases/LoginUseCase.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/usecases/LoginUseCase.kt @@ -3,7 +3,6 @@ package com.kickstarter.viewmodels.usecases import com.kickstarter.libs.Environment import com.kickstarter.libs.rx.transformers.Transformers import com.kickstarter.models.User -import rx.Observable class LoginUseCase(environment: Environment) { private val currentUser = requireNotNull(environment.currentUser()) @@ -11,48 +10,36 @@ class LoginUseCase(environment: Environment) { private val apolloClient = requireNotNull(environment.apolloClient()) private val apolloClientV2 = requireNotNull(environment.apolloClientV2()) - fun login(newUser: User, accessToken: String) { - currentUser.login(newUser, accessToken) - currentUserV2.login(newUser, accessToken) - } - fun logout() { currentUser.logout() currentUserV2.logout() } - fun refresh(updatedUser: User) { - currentUser.refresh(updatedUser) - currentUserV2.refresh(updatedUser) + fun setToken(accessToken: String) { + currentUser.setToken(accessToken) + currentUserV2.setToken(accessToken) } - fun loginAndUpdateUserPrivacy(newUser: User, accessToken: String): Observable { - login(newUser, accessToken) - return GetUserPrivacyUseCase(apolloClient).getUserPrivacy() - .compose(Transformers.neverError()) - .map { - val user = newUser.toBuilder() - .email(it.me()?.email()) - .isCreator(it.me()?.isCreator) - .isDeliverable(it.me()?.isDeliverable) - .isEmailVerified(it.me()?.isEmailVerified) - .hasPassword(it.me()?.hasPassword()).build() - refresh(user) - return@map user - } + fun setUser(user: User) { + currentUser.login(user) + currentUserV2.login(user) } - fun loginAndUpdateUserPrivacyV2(newUser: User, accessToken: String): io.reactivex.Observable { - login(newUser, accessToken) + fun loginAndUpdateUserPrivacy(newUser: User, accessToken: String): io.reactivex.Observable { + currentUserV2.setToken(accessToken) + currentUser.setToken(accessToken) return GetUserPrivacyUseCaseV2(apolloClientV2).getUserPrivacy() .compose(Transformers.neverErrorV2()) .map { - newUser.toBuilder() + val updated = newUser.toBuilder() .email(it.email) .isCreator(it.isCreator) .isDeliverable(it.isDeliverable) .isEmailVerified(it.isEmailVerified) .hasPassword(it.hasPassword).build() + currentUserV2.login(updated) + currentUser.login(updated) + return@map updated } } } diff --git a/app/src/test/java/com/kickstarter/libs/MockCurrentUser.java b/app/src/test/java/com/kickstarter/libs/MockCurrentUser.java index 80976aeefd..97c4cee56e 100644 --- a/app/src/test/java/com/kickstarter/libs/MockCurrentUser.java +++ b/app/src/test/java/com/kickstarter/libs/MockCurrentUser.java @@ -20,11 +20,15 @@ public MockCurrentUser(final @NonNull User initialUser) { } @Override - public void login(final @NonNull User newUser, final @NonNull String accessToken) { - this.user.onNext(newUser); + public void setToken(final @NonNull String accessToken) { this.accessToken = accessToken; } + @Override + public void login(final @NonNull User newUser) { + this.user.onNext(newUser); + } + @Override public void logout() { this.user.onNext(null); diff --git a/app/src/test/java/com/kickstarter/libs/MockCurrentUserV2.kt b/app/src/test/java/com/kickstarter/libs/MockCurrentUserV2.kt index 1468ec2530..43d90aaeea 100644 --- a/app/src/test/java/com/kickstarter/libs/MockCurrentUserV2.kt +++ b/app/src/test/java/com/kickstarter/libs/MockCurrentUserV2.kt @@ -17,10 +17,12 @@ class MockCurrentUserV2 : CurrentUserTypeV2 { user.onNext(KsOptional.of(initialUser)) } - override fun login(newUser: User, accessToken: String) { - user.onNext(KsOptional.of(newUser)) + override fun setToken(accessToken: String) { this.accessTokenPref = accessToken } + override fun login(newUser: User) { + user.onNext(KsOptional.of(newUser)) + } override fun logout() { user.onNext(KsOptional.empty()) diff --git a/app/src/test/java/com/kickstarter/viewmodels/ActivityFeedViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/ActivityFeedViewModelTest.kt index 50a4d3b568..b1d644e185 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/ActivityFeedViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/ActivityFeedViewModelTest.kt @@ -201,7 +201,8 @@ class ActivityFeedViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(initialUser, "deadbeef") + loginUserCase.setToken("deadbeef") + loginUserCase.setUser(initialUser) setUpEnvironment(environment) erroredBackings.assertValueCount(1) @@ -327,7 +328,8 @@ class ActivityFeedViewModelTest : KSRobolectricTestCase() { setUpEnvironment(environment) - loginUserCase.login(user(), "deadbeef") + loginUserCase.setToken("deadbeef") + loginUserCase.setUser(user()) surveys.assertValueCount(1) @@ -354,7 +356,8 @@ class ActivityFeedViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(initialUser, "deadbeef") + loginUserCase.setToken("deadbeef") + loginUserCase.setUser(initialUser) environment.currentUserV2()?.loggedInUser()?.subscribe { user.onNext(it) } ?.addToDisposable(disposables) @@ -391,7 +394,9 @@ class ActivityFeedViewModelTest : KSRobolectricTestCase() { .build() val loginUseCase = LoginUseCase(environment) - loginUseCase.login(initialUser, "token") + + loginUseCase.setToken("token") + loginUseCase.setUser(initialUser) environment.currentUserV2()?.loggedInUser()?.subscribe { user.onNext(it) } ?.addToDisposable(disposables) diff --git a/app/src/test/java/com/kickstarter/viewmodels/LoginToutViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/LoginToutViewModelTest.kt index a9ced4ae7b..0eaf4748d3 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/LoginToutViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/LoginToutViewModelTest.kt @@ -4,21 +4,23 @@ import com.facebook.FacebookAuthorizationException import com.kickstarter.KSRobolectricTestCase import com.kickstarter.libs.Environment import com.kickstarter.libs.MockCurrentUser +import com.kickstarter.libs.MockCurrentUserV2 import com.kickstarter.libs.featureflag.FlagKey import com.kickstarter.libs.utils.EventName import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.mock.MockFeatureFlagClient import com.kickstarter.mock.factories.ApiExceptionFactory +import com.kickstarter.mock.factories.UserFactory import com.kickstarter.mock.services.MockApiClientV2 import com.kickstarter.mock.services.MockApolloClientV2 import com.kickstarter.models.User +import com.kickstarter.models.UserPrivacy import com.kickstarter.services.apiresponses.AccessTokenEnvelope import com.kickstarter.services.apiresponses.ErrorEnvelope import com.kickstarter.ui.activities.DisclaimerItems import com.kickstarter.ui.data.LoginReason import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable -import io.reactivex.subjects.BehaviorSubject import io.reactivex.subscribers.TestSubscriber import org.junit.Test @@ -130,17 +132,30 @@ class LoginToutViewModelTest : KSRobolectricTestCase() { @Test fun facebookLogin_success() { - val currentUser = MockCurrentUser() + val currentUser = MockCurrentUserV2() + val userFb = UserFactory.user() + val apiClient = object : MockApiClientV2() { + override fun loginWithFacebook(accessToken: String): Observable { + return Observable.just(AccessTokenEnvelope.builder().user(userFb).accessToken("token").build()) + } + } + + val apolloClient = object : MockApolloClientV2() { + override fun userPrivacy(): Observable { + return Observable.just( + UserPrivacy(userFb.name(), "some@email.com", true, true, true, true, "USD") + ) + } + } + val environment = environment() .toBuilder() - .apiClientV2(MockApiClientV2()) - .apolloClientV2(MockApolloClientV2()) - .currentUser(currentUser) + .apiClientV2(apiClient) + .apolloClientV2(apolloClient) + .currentUserV2(currentUser) .build() - val user = BehaviorSubject.create() setUpEnvironment(environment, LoginReason.DEFAULT) - environment.currentUser()?.loggedInUser()?.subscribe { user.onNext(it) } this.currentUser.values().clear() @@ -154,7 +169,7 @@ class LoginToutViewModelTest : KSRobolectricTestCase() { this.currentUser.assertValueCount(2) finishWithSuccessfulResult.assertValueCount(1) - assertEquals("some@email.com", user.value?.email()) + assertEquals("some@email.com", this.currentUser.values().last()?.email()) segmentTrack.assertValues(EventName.PAGE_VIEWED.eventName, EventName.CTA_CLICKED.eventName) } diff --git a/app/src/test/java/com/kickstarter/viewmodels/MessageThreadsViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/MessageThreadsViewModelTest.kt index 9b916b2c54..7a1d58eae6 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/MessageThreadsViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/MessageThreadsViewModelTest.kt @@ -96,7 +96,8 @@ class MessageThreadsViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(user().toBuilder().unreadMessagesCount(0).build(), "beefbod5") + loginUserCase.setToken("beefbod5") + loginUserCase.setUser(user().toBuilder().unreadMessagesCount(0).build()) val intent = Intent().putExtra(IntentKey.PROJECT, project) vm.intent(intent) @@ -142,7 +143,8 @@ class MessageThreadsViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(user().toBuilder().unreadMessagesCount(0).build(), "beefbod5") + loginUserCase.setToken("beefbod5") + loginUserCase.setUser(user().toBuilder().unreadMessagesCount(0).build()) val intent = Intent().putExtra(IntentKey.PROJECT, Empty.INSTANCE) vm.intent(intent) @@ -197,7 +199,8 @@ class MessageThreadsViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(user().toBuilder().unreadMessagesCount(0).build(), "beefbod5") + loginUserCase.setToken("beefbod5") + loginUserCase.setUser(user().toBuilder().unreadMessagesCount(0).build()) val intent = Intent().putExtra(IntentKey.PROJECT, project) vm.intent(intent) diff --git a/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt index e45aa60e21..d8a914e512 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/OAuthActivityViewModelTest.kt @@ -7,6 +7,7 @@ import com.kickstarter.libs.Environment import com.kickstarter.libs.MockCurrentUserV2 import com.kickstarter.libs.utils.CodeVerifier import com.kickstarter.libs.utils.PKCE +import com.kickstarter.libs.utils.Secrets import com.kickstarter.mock.factories.ApiExceptionFactory import com.kickstarter.mock.factories.UserFactory import com.kickstarter.mock.services.MockApiClientV2 @@ -27,9 +28,9 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { } @Test - fun testProduceState_isAuthorizationStep() = runTest { + fun testProduceState_isAuthorizationStep_Staging() = runTest { - val testEndpoint = "testEndpoint" + val testEndpoint = Secrets.WebEndpoint.STAGING val testCodeVerifier = "testCodeVerifier" val testCodeChallenge = "testCodeChallenge" val environment = environment().toBuilder().webEndpoint(testEndpoint).build() @@ -52,7 +53,45 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { vm.uiState.toList(state) } - val testAuthorizationUrl = "$testEndpoint/oauth/authorizations/new?redirect_uri=ksrauth2&scope=1&client_id=2QEKDK20F5LO2CEOIDZZOW8QGOM6P68AB4A5OQ44XK3N0CUW5T&response_type=1&code_challenge=$testCodeChallenge&code_challenge_method=S256" + val clientID = Secrets.Api.Client.STAGING + val testAuthorizationUrl = "$testEndpoint/oauth/authorizations/new?redirect_uri=ksrauth2&scope=1&client_id=$clientID&response_type=1&code_challenge=$testCodeChallenge&code_challenge_method=S256" + // - First empty emission due the initialization + assertEquals( + listOf( + OAuthUiState(authorizationUrl = "", isAuthorizationStep = false, user = null), + OAuthUiState(authorizationUrl = testAuthorizationUrl, isAuthorizationStep = true, user = null) + ), + state + ) + } + + fun testProduceState_isAuthorizationStep_Production() = runTest { + + val testEndpoint = Secrets.WebEndpoint.PRODUCTION + val testCodeVerifier = "testCodeVerifier" + val testCodeChallenge = "testCodeChallenge" + val environment = environment().toBuilder().webEndpoint(testEndpoint).build() + + val mockCodeVerifier = object : PKCE { + override fun generateCodeChallenge(codeVerifier: String): String { + return testCodeChallenge + } + + override fun generateRandomCodeVerifier(entropy: Int): String { + return testCodeVerifier + } + } + + setUpEnvironment(environment, mockCodeVerifier) + + val state = mutableListOf() + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + vm.produceState(Intent()) + vm.uiState.toList(state) + } + + val clientID = Secrets.Api.Client.PRODUCTION + val testAuthorizationUrl = "$testEndpoint/oauth/authorizations/new?redirect_uri=ksrauth2&scope=1&client_id=$clientID&response_type=1&code_challenge=$testCodeChallenge&code_challenge_method=S256" // - First empty emission due the initialization assertEquals( listOf( @@ -98,7 +137,7 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { return Observable.just(OAuthTokenEnvelope.builder().accessToken("token").build()) } - override fun fetchCurrentUser(token: String): Observable { + override fun fetchCurrentUser(): Observable { return Observable.just(user) } } @@ -146,7 +185,7 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { return Observable.just(OAuthTokenEnvelope.builder().accessToken("tokensito").build()) } - override fun fetchCurrentUser(token: String): Observable { + override fun fetchCurrentUser(): Observable { return Observable.error(ApiExceptionFactory.badRequestException()) } } @@ -195,7 +234,7 @@ class OAuthActivityViewModelTest : KSRobolectricTestCase() { return Observable.error(ApiExceptionFactory.badRequestException()) } - override fun fetchCurrentUser(token: String): Observable { + override fun fetchCurrentUser(): Observable { return Observable.just(user) } } diff --git a/app/src/test/java/com/kickstarter/viewmodels/SetPasswordViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/SetPasswordViewModelTest.kt index 62246a72de..db3885b536 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/SetPasswordViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/SetPasswordViewModelTest.kt @@ -121,7 +121,8 @@ class SetPasswordViewModelTest : KSRobolectricTestCase() { val loginUserCase = LoginUseCase(environment) - loginUserCase.login(user, "token") + loginUserCase.setToken("token") + loginUserCase.setUser(user) this.vm.inputs.newPassword("password") this.vm.inputs.confirmPassword("password")