Unverified Commit c89473be authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge pull request #1700 from RocketChat/new/redesign-authentication-views

[NEW] Redesign authentication views
parents 2b9e7144 d443a9c5
...@@ -9,24 +9,25 @@ ...@@ -9,24 +9,25 @@
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
tools:replace="android:name"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_config" android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"> android:supportsRtl="true"
tools:replace="android:name">
<activity <activity
android:name=".authentication.ui.AuthenticationActivity" android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation" android:configChanges="orientation"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AuthenticationTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
...@@ -40,6 +41,7 @@ ...@@ -40,6 +41,7 @@
<data <data
android:host="auth" android:host="auth"
android:scheme="rocketchat" /> android:scheme="rocketchat" />
<data <data
android:host="go.rocket.chat" android:host="go.rocket.chat"
android:path="/auth" android:path="/auth"
...@@ -49,7 +51,7 @@ ...@@ -49,7 +51,7 @@
<activity <activity
android:name=".server.ui.ChangeServerActivity" android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".main.ui.MainActivity" android:name=".main.ui.MainActivity"
......
...@@ -2,13 +2,22 @@ package chat.rocket.android.analytics.event ...@@ -2,13 +2,22 @@ package chat.rocket.android.analytics.event
sealed class ScreenViewEvent(val screenName: String) { sealed class ScreenViewEvent(val screenName: String) {
// Authentication
object OnBoarding : ScreenViewEvent("OnBoardingFragment")
object Server : ScreenViewEvent("ServerFragment")
object LoginOptions : ScreenViewEvent("LoginOptionsFragment")
object Login : ScreenViewEvent("LoginFragment")
object TwoFa : ScreenViewEvent("TwoFAFragment")
object SignUp : ScreenViewEvent("SignupFragment")
object RegisterUsername : ScreenViewEvent("RegisterUsernameFragment")
object ResetPassword : ScreenViewEvent("ResetPasswordFragment")
object About : ScreenViewEvent("AboutFragment") object About : ScreenViewEvent("AboutFragment")
object ChatRoom : ScreenViewEvent("ChatRoomFragment") object ChatRoom : ScreenViewEvent("ChatRoomFragment")
object ChatRooms : ScreenViewEvent("ChatRoomsFragment") object ChatRooms : ScreenViewEvent("ChatRoomsFragment")
object CreateChannel : ScreenViewEvent("CreateChannelFragment") object CreateChannel : ScreenViewEvent("CreateChannelFragment")
object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment") object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment")
object Files : ScreenViewEvent("FilesFragment") object Files : ScreenViewEvent("FilesFragment")
object Login : ScreenViewEvent("LoginFragment")
object MemberBottomSheet : ScreenViewEvent("MemberBottomSheetFragment") object MemberBottomSheet : ScreenViewEvent("MemberBottomSheetFragment")
object Members : ScreenViewEvent("MembersFragment") object Members : ScreenViewEvent("MembersFragment")
object Mentions : ScreenViewEvent("MentionsFragment") object Mentions : ScreenViewEvent("MentionsFragment")
...@@ -17,10 +26,5 @@ sealed class ScreenViewEvent(val screenName: String) { ...@@ -17,10 +26,5 @@ sealed class ScreenViewEvent(val screenName: String) {
object PinnedMessages : ScreenViewEvent("PinnedMessagesFragment") object PinnedMessages : ScreenViewEvent("PinnedMessagesFragment")
object Preferences : ScreenViewEvent("PreferencesFragment") object Preferences : ScreenViewEvent("PreferencesFragment")
object Profile : ScreenViewEvent("ProfileFragment") object Profile : ScreenViewEvent("ProfileFragment")
object RegisterUsername : ScreenViewEvent("RegisterUsernameFragment")
object ResetPassword : ScreenViewEvent("ResetPasswordFragment")
object Server : ScreenViewEvent("ServerFragment")
object Settings : ScreenViewEvent("SettingsFragment") object Settings : ScreenViewEvent("SettingsFragment")
object SignUp : ScreenViewEvent("SignupFragment")
object TwoFa : ScreenViewEvent("TwoFAFragment")
} }
package chat.rocket.android.authentication.di package chat.rocket.android.authentication.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.qualifier.ForAuthentication import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class AuthenticationModule { class AuthenticationModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity) = AuthenticationNavigator(activity) fun provideAuthenticationNavigator(activity: AuthenticationActivity) =
AuthenticationNavigator(activity)
@Provides
@PerActivity
fun provideJob() = Job()
@Provides
@PerActivity
fun provideLifecycleOwner(activity: AuthenticationActivity): LifecycleOwner = activity
@Provides
@PerActivity
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy =
CancelStrategy(owner, jobs)
} }
\ No newline at end of file
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.login.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.login.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.authentication.login.ui.LoginFragment import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class LoginFragmentModule { class LoginFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun loginView(frag: LoginFragment): LoginView = frag
@Provides @Provides
@PerFragment @PerFragment
fun loginView(frag: LoginFragment): LoginView { fun provideLifecycleOwner(frag: LoginFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: LoginFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -5,259 +5,34 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -5,259 +5,34 @@ import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView { interface LoginView : LoadingView, MessageView {
/**
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
*
* REMARK: We must set up the login button listener [setupLoginButtonListener].
* Remember to enable [enableUserInput] or disable [disableUserInput] the view interaction for the user when submitting the form.
*/
fun showFormView()
/**
* Hides the form view.
*/
fun hideFormView()
/**
* Setups the login button when tapped.
*/
fun setupLoginButtonListener()
/**
* Enables the view interactions for the user.
*/
fun enableUserInput()
/**
* Disables the view interactions for the user.
*/
fun disableUserInput()
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
*
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/
fun showCasButton()
/**
* Hides the CAS button.
*/
fun hideCasButton()
/**
* Setups the CAS button when tapped.
*
* @param casUrl The CAS URL to authenticate with.
* @param casToken The requested token to be sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
/**
* Shows the sign up view if the new users registration is enabled by the server settings.
*
* REMARK: We must set up the sign up view listener [setupSignUpView].
*/
fun showSignUpView()
/**
* Setups the sign up view when tapped.
*/
fun setupSignUpView()
/** /**
* Shows the forgot password view if enabled by the server settings. * Shows the forgot password view if enabled by the server settings.
*
* REMARK: We must set up the forgot password view listener [setupForgotPasswordView].
*/ */
fun showForgotPasswordView() fun showForgotPasswordView()
/** /**
* Setups the forgot password view when tapped. * Saves Google Smart Lock credentials.
*/
fun setupForgotPasswordView()
/**
* Hides the sign up view.
*/
fun hideSignUpView()
/**
* Enables and shows the oauth view if there is login via social accounts enabled by the server settings.
*
* REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab], [addCustomOauthServiceButton] or [addSamlServiceButton]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*/
fun enableOauthView()
/**
* Disables and hides the Oauth view if there is not login via social accounts enabled by the server settings.
*/
fun disableOauthView()
/**
* Shows the login button.
*/
fun showLoginButton()
/**
* Hides the login button.
*/
fun hideLoginButton()
/**
* Shows the "login by Facebook view if it is enable by the server settings.
*/
fun enableLoginByFacebook()
/**
* Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/
fun enableLoginByGithub()
/**
* Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it [setupGoogleButtonListener].
*/
fun enableLoginByGoogle()
/**
* Setups the Google button when tapped.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enable by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it [setupLinkedinButtonListener].
*/
fun enableLoginByLinkedin()
/**
* Setups the Linkedin button when tapped.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Setups the Facebook button when tapped.
*
* @param facebookOauthUrl The Facebook OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupFacebookButtonListener(facebookOauthUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/
fun enableLoginByMeteor()
/**
* Shows the "login by Twitter" view if it is enable by the server settings.
*/
fun enableLoginByTwitter()
/**
* Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/
fun enableLoginByGitlab()
/**
* Setups the Gitlab button when tapped.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/**
* Shows the "login by WordPress" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupWordpressButtonListener].
*/
fun enableLoginByWordpress()
/**
* Setups the WordPress button when tapped.
*
* @param wordpressUrl The WordPress OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupWordpressButtonListener(wordpressUrl: String, state: String)
/**
* Adds a custom OAuth button in the oauth view.
*
* @customOauthUrl The custom OAuth url to sets up the button (the listener).
* @state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
* @serviceName The custom OAuth service name.
* @serviceNameColor The custom OAuth service name color (just stylizing).
* @buttonColor The custom OAuth button color (just stylizing).
* @see [enableOauthView]
*/
fun addCustomOauthServiceButton(
customOauthUrl: String,
state: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/**
* Adds a SAML button in the oauth view.
*
* @samlUrl The SAML url to sets up the button (the listener).
* @serviceName The SAML service name.
* @serviceNameColor The SAML service name color (just stylizing).
* @buttonColor The SAML button color (just stylizing).
* @see [enableOauthView]
*/ */
fun addSamlServiceButton( fun saveSmartLockCredentials(id: String, password: String)
samlUrl: String,
samlToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Enables the button to login when the user inputs an username and a password.
*/ */
fun setupFabListener() fun enableButtonLogin()
fun setupGlobalListener()
/** /**
* Alerts the user about a wrong inputted username or email. * Disables the button to login when there is not an entered/not blank username or password by
* the user (i.e. The fields are empty/blank)
*/ */
fun alertWrongUsernameOrEmail() fun disableButtonLogin()
/** /**
* Alerts the user about a wrong inputted password. * Enables the forget password button after requesting a processing to log in.
*/ */
fun alertWrongPassword() fun enableButtonForgetPassword()
/** /**
* Saves Google Smart Lock credentials. * Disables the forget password button when requesting a processing to log in.
*/ */
fun saveSmartLockCredentials(id: String, password: String) fun disableButtonForgetPassword()
} }
\ No newline at end of file
package chat.rocket.android.authentication.loginoptions.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.loginoptions.presentation.LoginOptionsView
import chat.rocket.android.authentication.loginoptions.ui.LoginOptionsFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
@Module
class LoginOptionsFragmentModule {
@Provides
@PerFragment
fun loginOptionsView(frag: LoginOptionsFragment): LoginOptionsView = frag
@Provides
@PerFragment
fun provideLifecycleOwner(frag: LoginOptionsFragment): LifecycleOwner = frag
}
\ No newline at end of file
package chat.rocket.android.authentication.loginoptions.di
import chat.rocket.android.authentication.loginoptions.ui.LoginOptionsFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class LoginOptionsFragmentProvider {
@ContributesAndroidInjector(modules = [LoginOptionsFragmentModule::class])
@PerFragment
abstract fun providesLoginOptionFragment(): LoginOptionsFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.loginoptions.presentation
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.Email
import chat.rocket.common.model.Token
import chat.rocket.common.model.User
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.loginWithCas
import chat.rocket.core.internal.rest.loginWithOauth
import chat.rocket.core.internal.rest.loginWithSaml
import chat.rocket.core.internal.rest.me
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import javax.inject.Inject
private const val TYPE_LOGIN_OAUTH = 1
private const val TYPE_LOGIN_CAS = 2
private const val TYPE_LOGIN_SAML = 3
private const val TYPE_LOGIN_DEEP_LINK = 4
class LoginOptionsPresenter @Inject constructor(
private val view: LoginOptionsView,
private val strategy: CancelStrategy,
private val factory: RocketChatClientFactory,
private val navigator: AuthenticationNavigator,
private val settingsInteractor: GetSettingsInteractor,
private val localRepository: LocalRepository,
private val saveCurrentServer: SaveCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
private val analyticsManager: AnalyticsManager,
private val tokenRepository: TokenRepository,
serverInteractor: GetConnectingServerInteractor
) {
// TODO - we should validate the current server when opening the app, and have a nonnull get()
private var currentServer = serverInteractor.get()!!
private lateinit var client: RocketChatClient
private lateinit var settings: PublicSettings
private lateinit var credentialToken: String
private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String
private lateinit var loginMethod: AuthenticationEvent
fun toCreateAccount() = navigator.toCreateAccount()
fun toLoginWithEmail() = navigator.toLogin(currentServer)
fun authenticateWithOauth(oauthToken: String, oauthSecret: String) {
credentialToken = oauthToken
credentialSecret = oauthSecret
loginMethod = AuthenticationEvent.AuthenticationWithOauth
doAuthentication(TYPE_LOGIN_OAUTH)
}
fun authenticateWithCas(casToken: String) {
credentialToken = casToken
loginMethod = AuthenticationEvent.AuthenticationWithCas
doAuthentication(TYPE_LOGIN_CAS)
}
fun authenticateWithSaml(samlToken: String) {
credentialToken = samlToken
loginMethod = AuthenticationEvent.AuthenticationWithSaml
doAuthentication(TYPE_LOGIN_SAML)
}
fun authenticateWithDeepLink(deepLinkInfo: LoginDeepLinkInfo) {
val serverUrl = deepLinkInfo.url
setupConnectionInfo(serverUrl)
if (deepLinkInfo.userId != null && deepLinkInfo.token != null) {
deepLinkUserId = deepLinkInfo.userId
deepLinkToken = deepLinkInfo.token
tokenRepository.save(serverUrl, Token(deepLinkUserId, deepLinkToken))
loginMethod = AuthenticationEvent.AuthenticationWithDeeplink
doAuthentication(TYPE_LOGIN_DEEP_LINK)
}
}
private fun doAuthentication(loginType: Int) {
launchUI(strategy) {
view.showLoading()
try {
val token = retryIO("login") {
when (loginType) {
TYPE_LOGIN_OAUTH -> client.loginWithOauth(credentialToken, credentialSecret)
TYPE_LOGIN_CAS -> {
delay(3, TimeUnit.SECONDS)
client.loginWithCas(credentialToken)
}
TYPE_LOGIN_SAML -> {
delay(3, TimeUnit.SECONDS)
client.loginWithSaml(credentialToken)
}
TYPE_LOGIN_DEEP_LINK -> {
val myself = client.me() // Just checking if the credentials worked.
if (myself.id == deepLinkUserId) {
Token(deepLinkUserId, deepLinkToken)
} else {
throw RocketChatAuthException("Invalid AuthenticationEvent Deep Link Credentials...")
}
}
else -> {
throw IllegalStateException(
"Expected TYPE_LOGIN_USER_EMAIL, " +
"TYPE_LOGIN_CAS,TYPE_LOGIN_SAML, TYPE_LOGIN_OAUTH or " +
"TYPE_LOGIN_DEEP_LINK"
)
}
}
}
val myself = retryIO("me()") { client.me() }
myself.username?.let { username ->
val user = User(
id = myself.id,
roles = myself.roles,
status = myself.status,
name = myself.name,
emails = myself.emails?.map { Email(it.address ?: "", it.verified) },
username = myself.username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(url = currentServer, user = user)
saveCurrentServer.save(currentServer)
saveAccount(username)
saveToken(token)
analyticsManager.logLogin(loginMethod, true)
navigator.toChatList()
}.ifNull {
if (loginType == TYPE_LOGIN_OAUTH) {
navigator.toRegisterUsername(token.userId, token.authToken)
}
}
} catch (exception: RocketChatException) {
analyticsManager.logLogin(loginMethod, false)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
private fun setupConnectionInfo(serverUrl: String) {
currentServer = serverUrl
client = factory.create(currentServer)
settings = settingsInteractor.get(currentServer)
}
private suspend fun saveAccount(username: String) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
val logo = settings.wideTile()?.let {
currentServer.serverLogoUrl(it)
}
val thumb = currentServer.avatarUrl(username)
val account = Account(currentServer, icon, logo, username, thumb)
saveAccountInteractor.save(account)
}
private fun saveToken(token: Token) = tokenRepository.save(currentServer, token)
}
\ No newline at end of file
package chat.rocket.android.authentication.loginoptions.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface LoginOptionsView : LoadingView, MessageView {
// OAuth accounts.
/**
* Shows the "login by Facebook" view if it is enabled by the server settings.
*
* REMARK: We must set up the Facebook button listener before enabling it
* [setupFacebookButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByFacebook()
/**
* Setups the Facebook button.
*
* @param facebookOauthUrl The Facebook OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
*/
fun setupFacebookButtonListener(facebookOauthUrl: String, state: String)
/**
* Shows the "login by Github" view if it is enabled by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it
* [setupGithubButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByGithub()
/**
* Setups the Github button.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enabled by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it
* [setupGoogleButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByGoogle()
/**
* Setups the Google button.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enabled by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it
* [setupLinkedinButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByLinkedin()
/**
* Setups the Linkedin button.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Shows the "login by Gitlab" view if it is enabled by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it
* [setupGitlabButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByGitlab()
/**
* Setups the Gitlab button.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/**
* Shows the "login by WordPress" view if it is enabled by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupWordpressButtonListener].
*/
fun enableLoginByWordpress()
/**
* Setups the WordPress button when tapped.
*
* @param wordpressUrl The WordPress OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupWordpressButtonListener(wordpressUrl: String, state: String)
// CAS account.
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server
* settings.
*
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
* @see [showAccountsView]
*/
fun enableLoginByCas()
/**
* Setups the CAS button.
*
* @param casUrl The CAS URL to authenticate with.
* @param casToken The requested token to be sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
// Custom OAuth account.
/**
* Adds a custom OAuth button in the accounts container.
*
* @customOauthUrl The custom OAuth url.
* @state A random string generated by the app, which you'll verify later
* (to protect against forgery attacks).
* @serviceName The custom OAuth service name.
* @serviceNameColor The custom OAuth service name color (just stylizing).
* @buttonColor The custom OAuth button color (just stylizing).
* @see [showAccountsView]
*/
fun addCustomOauthButton(
customOauthUrl: String,
state: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
// SAML account.
/**
* Adds a SAML button in the accounts container.
*
* @samlUrl The SAML url.
* @serviceName The SAML service name.
* @serviceNameColor The SAML service name color (just stylizing).
* @buttonColor The SAML button color (just stylizing).
* @see [showAccountsView]
*/
fun addSamlButton(
samlUrl: String,
samlToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/**
* Shows the accounts container view if there is at least a login via
* OAuth/Custom OAuth/CAS/SAML account enabled by the server settings.
*
* REMARK: We must show at maximum *three* accounts views ([enableLoginByFacebook],
* [enableLoginByGithub], [enableLoginByGoogle], [enableLoginByLinkedin], [enableLoginByGitlab],
* [enableLoginByCas], [addCustomOauthButton] or [addSamlButton]) for the accounts container view.
* If the enabled accounts exceeds 3 we must set up the [setupExpandAccountsView] to show the
* remaining view(s).
*/
fun showAccountsView()
/**
* Setups the expand accounts view to show more accounts views (expanding the accounts view
* interface to show the remaining view(s)).
*/
fun setupExpandAccountsView()
/**
* Shows the "login with e-mail" view if it is enabled by the server settings.
*/
fun showLoginWithEmailButton()
/**
* Shows the "Create new account" view if it is enabled by the server settings.
*/
fun showCreateNewAccountButton()
}
package chat.rocket.android.authentication.onboarding.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.onboarding.presentation.OnBoardingView
import chat.rocket.android.authentication.onboarding.ui.OnBoardingFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
@Module
class OnBoardingFragmentModule {
@Provides
@PerFragment
fun onBoardingView(frag: OnBoardingFragment): OnBoardingView = frag
@Provides
@PerFragment
fun provideLifecycleOwner(frag: OnBoardingFragment): LifecycleOwner = frag
}
\ No newline at end of file
package chat.rocket.android.authentication.onboarding.di
import chat.rocket.android.authentication.onboarding.ui.OnBoardingFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class OnBoardingFragmentProvider {
@ContributesAndroidInjector(modules = [OnBoardingFragmentModule::class])
@PerFragment
abstract fun provideOnBoardingFragment(): OnBoardingFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.onboarding.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class OnBoardingPresenter @Inject constructor(
private val view: OnBoardingView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor) {
fun toSignInToYourServer() = navigator.toSignInToYourServer()
fun connectToCommunityServer(communityServerUrl: String) {
connectToServer(communityServerUrl) {
if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) {
navigator.toLogin(communityServerUrl)
} else {
navigator.toLoginOptions(
communityServerUrl,
state,
facebookOauthUrl,
githubOauthUrl,
googleOauthUrl,
linkedinOauthUrl,
gitlabOauthUrl,
wordpressOauthUrl,
casLoginUrl,
casToken,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
customOauthServiceButtonColor,
samlUrl,
samlToken,
samlServiceName,
samlServiceNameTextColor,
samlServiceButtonColor,
totalSocialAccountsEnabled,
isLoginFormEnabled,
isNewAccountCreationEnabled
)
}
}
}
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
private fun connectToServer(serverUrl: String, block: () -> Unit) {
launchUI(strategy) {
// Check if we already have an account for this server...
val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == serverUrl }
if (account != null) {
navigator.toChatList(serverUrl)
return@launchUI
}
view.showLoading()
try {
withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it
checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled()
serverInteractor.save(serverUrl)
block()
}
} catch (ex: Exception) {
view.showMessage(ex)
} finally {
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.onboarding.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface OnBoardingView : LoadingView, MessageView
\ No newline at end of file
package chat.rocket.android.authentication.onboarding.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.onboarding.presentation.OnBoardingPresenter
import chat.rocket.android.authentication.onboarding.presentation.OnBoardingView
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setLightStatusBar
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_authentication_on_boarding.*
import javax.inject.Inject
fun newInstance() = OnBoardingFragment()
class OnBoardingFragment : Fragment(), OnBoardingView {
@Inject
lateinit var presenter: OnBoardingPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_on_boarding)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupOnClickListener()
analyticsManager.logScreenView(ScreenViewEvent.OnBoarding)
}
private fun setupToolbar() {
with(activity as AuthenticationActivity) {
view?.let { this.setLightStatusBar(it) }
toolbar.isVisible = false
}
}
private fun setupOnClickListener() {
connect_with_a_server_container.setOnClickListener { signInToYourServer() }
join_community_container.setOnClickListener { joinInTheCommunity() }
create_server_container.setOnClickListener { createANewServer() }
}
override fun showLoading() {
ui {
view_loading.isVisible = true
}
}
override fun hideLoading() {
ui {
view_loading.isVisible = false
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun signInToYourServer() = ui {
presenter.toSignInToYourServer()
}
private fun joinInTheCommunity() = ui {
presenter.connectToCommunityServer(
getString(R.string.default_protocol) + getString(R.string.community_server_url)
)
}
private fun createANewServer() = ui {
presenter.toCreateANewServer(
getString(R.string.default_protocol) + getString(R.string.create_server_url)
)
}
}
...@@ -2,19 +2,9 @@ package chat.rocket.android.authentication.presentation ...@@ -2,19 +2,9 @@ package chat.rocket.android.authentication.presentation
import android.content.Intent import android.content.Intent
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.login.ui.TAG_LOGIN_FRAGMENT
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.registerusername.ui.TAG_REGISTER_USERNAME_FRAGMENT
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.authentication.resetpassword.ui.TAG_RESET_PASSWORD_FRAGMENT
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.signup.ui.TAG_SIGNUP_FRAGMENT
import chat.rocket.android.authentication.twofactor.ui.TAG_TWO_FA_FRAGMENT
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
...@@ -23,51 +13,114 @@ import chat.rocket.android.webview.ui.webViewIntent ...@@ -23,51 +13,114 @@ import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity) { class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
fun toLogin() { fun toSignInToYourServer() {
activity.addFragmentBackStack(TAG_LOGIN_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(ScreenViewEvent.Server.screenName, R.id.fragment_container) {
LoginFragment.newInstance() chat.rocket.android.authentication.server.ui.newInstance()
} }
} }
fun toLogin(deepLinkInfo: LoginDeepLinkInfo) { fun toLoginOptions(
activity.addFragmentBackStack(TAG_LOGIN_FRAGMENT, R.id.fragment_container) { serverUrl: String,
LoginFragment.newInstance(deepLinkInfo) state: String? = null,
facebookOauthUrl: String? = null,
githubOauthUrl: String? = null,
googleOauthUrl: String? = null,
linkedinOauthUrl: String? = null,
gitlabOauthUrl: String? = null,
wordpressOauthUrl: String? = null,
casLoginUrl: String? = null,
casToken: String? = null,
customOauthUrl: String? = null,
customOauthServiceName: String? = null,
customOauthServiceNameTextColor: Int = 0,
customOauthServiceButtonColor: Int = 0,
samlUrl: String? = null,
samlToken: String? = null,
samlServiceName: String? = null,
samlServiceNameTextColor: Int = 0,
samlServiceButtonColor: Int = 0,
totalSocialAccountsEnabled: Int = 0,
isLoginFormEnabled: Boolean = true,
isNewAccountCreationEnabled: Boolean = true,
deepLinkInfo: LoginDeepLinkInfo? = null
) {
activity.addFragmentBackStack(
ScreenViewEvent.LoginOptions.screenName,
R.id.fragment_container
) {
chat.rocket.android.authentication.loginoptions.ui.newInstance(
serverUrl,
state,
facebookOauthUrl,
githubOauthUrl,
googleOauthUrl,
linkedinOauthUrl,
gitlabOauthUrl,
wordpressOauthUrl,
casLoginUrl,
casToken,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
customOauthServiceButtonColor,
samlUrl,
samlToken,
samlServiceName,
samlServiceNameTextColor,
samlServiceButtonColor,
totalSocialAccountsEnabled,
isLoginFormEnabled,
isNewAccountCreationEnabled,
deepLinkInfo
)
} }
} }
fun toPreviousView() { fun toTwoFA(username: String, password: String) {
activity.toPreviousView() activity.addFragmentBackStack(ScreenViewEvent.TwoFa.screenName, R.id.fragment_container) {
chat.rocket.android.authentication.twofactor.ui.newInstance(username, password)
}
} }
fun toTwoFA(username: String, password: String) { fun toCreateAccount() {
activity.addFragmentBackStack(TAG_TWO_FA_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(ScreenViewEvent.SignUp.screenName, R.id.fragment_container) {
TwoFAFragment.newInstance(username, password) chat.rocket.android.authentication.signup.ui.newInstance()
} }
} }
fun toSignUp() { fun toLogin(serverUrl: String) {
activity.addFragmentBackStack(TAG_SIGNUP_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(ScreenViewEvent.Login.screenName, R.id.fragment_container) {
SignupFragment.newInstance() chat.rocket.android.authentication.login.ui.newInstance(serverUrl)
} }
} }
fun toForgotPassword() { fun toForgotPassword() {
activity.addFragmentBackStack(TAG_RESET_PASSWORD_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(
ResetPasswordFragment.newInstance() ScreenViewEvent.ResetPassword.screenName,
R.id.fragment_container
) {
chat.rocket.android.authentication.resetpassword.ui.newInstance()
} }
} }
fun toWebPage(url: String) { fun toPreviousView() {
activity.startActivity(activity.webViewIntent(url)) activity.toPreviousView()
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
fun toRegisterUsername(userId: String, authToken: String) { fun toRegisterUsername(userId: String, authToken: String) {
activity.addFragmentBackStack(TAG_REGISTER_USERNAME_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(
RegisterUsernameFragment.newInstance(userId, authToken) ScreenViewEvent.RegisterUsername.screenName,
R.id.fragment_container
) {
chat.rocket.android.authentication.registerusername.ui.newInstance(userId, authToken)
} }
} }
fun toWebPage(url: String, toolbarTitle: String? = null) {
activity.startActivity(activity.webViewIntent(url, toolbarTitle))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
fun toChatList() { fun toChatList() {
activity.startActivity(Intent(activity, MainActivity::class.java)) activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish() activity.finish()
...@@ -77,9 +130,4 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -77,9 +130,4 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
activity.startActivity(activity.changeServerIntent(serverUrl)) activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish() activity.finish()
} }
}
fun toServerScreen() {
activity.startActivity(activity.newServerIntent())
activity.finish()
}
}
\ No newline at end of file
package chat.rocket.android.authentication.presentation package chat.rocket.android.authentication.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountInteractor import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.privacyPolicyUrl
import chat.rocket.android.util.extensions.termsOfServiceUrl
import javax.inject.Inject import javax.inject.Inject
class AuthenticationPresenter @Inject constructor( class AuthenticationPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor, private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor, private val getAccountInteractor: GetAccountInteractor,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val tokenRepository: TokenRepository private val tokenRepository: TokenRepository,
private val serverInteractor: GetConnectingServerInteractor
) { ) {
suspend fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (newServer || currentServer == null || serverToken == null || settings == null || account?.userName == null) { fun loadCredentials(newServer: Boolean, callback: (isAuthenticated: Boolean) -> Unit) {
callback(false) launchUI(strategy) {
} else { val currentServer = getCurrentServerInteractor.get()
callback(true) val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
navigator.toChatList() val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (newServer || currentServer == null ||
serverToken == null ||
settings == null ||
account?.userName == null
) {
callback(false)
} else {
callback(true)
}
} }
} }
fun termsOfService(toolbarTitle: String) =
serverInteractor.get()?.let { navigator.toWebPage(it.termsOfServiceUrl(), toolbarTitle) }
fun privacyPolicy(toolbarTitle: String) =
serverInteractor.get()?.let { navigator.toWebPage(it.privacyPolicyUrl(), toolbarTitle) }
fun toChatList() = navigator.toChatList()
} }
\ No newline at end of file
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.registerusername.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.registerusername.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class RegisterUsernameFragmentModule { class RegisterUsernameFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun registerUsernameView(frag: RegisterUsernameFragment): RegisterUsernameView = frag
@Provides @Provides
@PerFragment @PerFragment
fun registerUsernameView(frag: RegisterUsernameFragment): RegisterUsernameView { fun provideLifecycleOwner(frag: RegisterUsernameFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: RegisterUsernameFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -30,48 +30,44 @@ class RegisterUsernamePresenter @Inject constructor( ...@@ -30,48 +30,44 @@ class RegisterUsernamePresenter @Inject constructor(
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val analyticsManager: AnalyticsManager, private val analyticsManager: AnalyticsManager,
serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor, private val saveCurrentServer: SaveCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor val serverInteractor: GetConnectingServerInteractor,
val factory: RocketChatClientFactory,
val settingsInteractor: GetSettingsInteractor
) { ) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun registerUsername(username: String, userId: String, authToken: String) { fun registerUsername(username: String, userId: String, authToken: String) {
if (username.isBlank()) { launchUI(strategy) {
view.alertBlankUsername() view.showLoading()
} else { try {
launchUI(strategy) { val me = retryIO("updateOwnBasicInformation(username = $username)") {
view.showLoading() client.updateOwnBasicInformation(username = username)
try {
val me = retryIO("updateOwnBasicInformation(username = $username)") {
client.updateOwnBasicInformation(username = username)
}
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
saveCurrentServer.save(currentServer)
tokenRepository.save(currentServer, Token(userId, authToken))
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithOauth,
true
)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
analyticsManager.logSignUp(AuthenticationEvent.AuthenticationWithOauth, false)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
saveCurrentServer.save(currentServer)
tokenRepository.save(currentServer, Token(userId, authToken))
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithOauth,
true
)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
analyticsManager.logSignUp(AuthenticationEvent.AuthenticationWithOauth, false)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
} }
} }
......
...@@ -6,7 +6,12 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -6,7 +6,12 @@ import chat.rocket.android.core.behaviours.MessageView
interface RegisterUsernameView : LoadingView, MessageView { interface RegisterUsernameView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank username. * Enables the button to set the username if the user entered at least one character.
*/ */
fun alertBlankUsername() fun enableButtonUseThisUsername()
/**
* Disables the button to set the username when there is no character entered by the user.
*/
fun disableButtonUseThisUsername()
} }
\ No newline at end of file
...@@ -6,25 +6,39 @@ import android.os.Bundle ...@@ -6,25 +6,39 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernamePresenter import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernamePresenter
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showKeyboard import chat.rocket.android.util.extensions.showKeyboard
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_authentication_register_username.* import kotlinx.android.synthetic.main.fragment_authentication_register_username.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
internal const val TAG_REGISTER_USERNAME_FRAGMENT = "RegisterUsernameFragment" private const val BUNDLE_USER_ID = "user_id"
private const val BUNDLE_AUTH_TOKEN = "auth_token"
fun newInstance(userId: String, authToken: String): Fragment {
return RegisterUsernameFragment().apply {
arguments = Bundle(2).apply {
putString(BUNDLE_USER_ID, userId)
putString(BUNDLE_AUTH_TOKEN, authToken)
}
}
}
class RegisterUsernameFragment : Fragment(), RegisterUsernameView { class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject @Inject
...@@ -33,26 +47,19 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -33,26 +47,19 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
lateinit var analyticsManager: AnalyticsManager lateinit var analyticsManager: AnalyticsManager
private lateinit var userId: String private lateinit var userId: String
private lateinit var authToken: String private lateinit var authToken: String
private lateinit var usernameDisposable: Disposable
companion object {
private const val USER_ID = "user_id"
private const val AUTH_TOKEN = "auth_token"
fun newInstance(userId: String, authToken: String) = RegisterUsernameFragment().apply {
arguments = Bundle(1).apply {
putString(USER_ID, userId)
putString(AUTH_TOKEN, authToken)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments. val bundle = arguments
userId = arguments?.getString(USER_ID) ?: "" if (bundle != null) {
authToken = arguments?.getString(AUTH_TOKEN) ?: "" userId = bundle.getString(BUNDLE_USER_ID)
authToken = bundle.getString(BUNDLE_AUTH_TOKEN)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
} }
override fun onCreateView( override fun onCreateView(
...@@ -74,14 +81,32 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -74,14 +81,32 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
} }
setupOnClickListener() setupOnClickListener()
subscribeEditText()
analyticsManager.logScreenView(ScreenViewEvent.RegisterUsername) analyticsManager.logScreenView(ScreenViewEvent.RegisterUsername)
} }
override fun alertBlankUsername() { override fun onDestroyView() {
ui { super.onDestroyView()
vibrateSmartPhone() unsubscribeEditText()
text_username.shake() }
override fun enableButtonUseThisUsername() {
context?.let {
ViewCompat.setBackgroundTintList(
button_use_this_username, ContextCompat.getColorStateList(it, R.color.colorAccent)
)
button_use_this_username.isEnabled = true
}
}
override fun disableButtonUseThisUsername() {
context?.let {
ViewCompat.setBackgroundTintList(
button_use_this_username,
ContextCompat.getColorStateList(it, R.color.colorAuthenticationButtonDisabled)
)
button_use_this_username.isEnabled = false
} }
} }
...@@ -117,7 +142,7 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -117,7 +142,7 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
ui { ui {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_20dp, it)
DrawableHelper.wrapDrawable(atDrawable) DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable) DrawableHelper.compoundDrawable(text_username, atDrawable)
...@@ -125,12 +150,12 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -125,12 +150,12 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
} }
private fun enableUserInput() { private fun enableUserInput() {
button_use_this_username.isEnabled = true enableButtonUseThisUsername()
text_username.isEnabled = true text_username.isEnabled = true
} }
private fun disableUserInput() { private fun disableUserInput() {
button_use_this_username.isEnabled = false disableButtonUseThisUsername()
text_username.isEnabled = true text_username.isEnabled = true
} }
...@@ -139,4 +164,18 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -139,4 +164,18 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
presenter.registerUsername(text_username.textContent, userId, authToken) presenter.registerUsername(text_username.textContent, userId, authToken)
} }
} }
private fun subscribeEditText() {
usernameDisposable = text_username.asObservable()
.debounce(300, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe {
if (it.isNotBlank()) {
enableButtonUseThisUsername()
} else {
disableButtonUseThisUsername()
}
}
}
private fun unsubscribeEditText() = usernameDisposable.dispose()
} }
\ No newline at end of file
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.resetpassword.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.resetpassword.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class ResetPasswordFragmentModule { class ResetPasswordFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun resetPasswordView(frag: ResetPasswordFragment): ResetPasswordView = frag
@Provides @Provides
@PerFragment @PerFragment
fun resetPasswordView(frag: ResetPasswordFragment): ResetPasswordView { fun provideLifecycleOwner(frag: ResetPasswordFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: ResetPasswordFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -4,7 +4,6 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator ...@@ -4,7 +4,6 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetConnectingServerInteractor import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
...@@ -25,30 +24,26 @@ class ResetPasswordPresenter @Inject constructor( ...@@ -25,30 +24,26 @@ class ResetPasswordPresenter @Inject constructor(
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
fun resetPassword(email: String) { fun resetPassword(email: String) {
when { launchUI(strategy) {
email.isBlank() -> view.alertBlankEmail() view.showLoading()
!email.isEmail() -> view.alertInvalidEmail() try {
else -> launchUI(strategy) { retryIO("forgotPassword(email = $email)") {
view.showLoading() client.forgotPassword(email)
try { }
retryIO("forgotPassword(email = $email)") { navigator.toPreviousView()
client.forgotPassword(email) view.emailSent()
} } catch (exception: RocketChatException) {
navigator.toPreviousView() if (exception is RocketChatInvalidResponseException) {
view.emailSent() view.updateYourServerVersion()
} catch (exception: RocketChatException) { } else {
if (exception is RocketChatInvalidResponseException) { exception.message?.let {
view.updateYourServerVersion() view.showMessage(it)
} else { }.ifNull {
exception.message?.let { view.showGenericErrorMessage()
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} }
} finally {
view.hideLoading()
} }
} finally {
view.hideLoading()
} }
} }
} }
......
...@@ -6,22 +6,22 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -6,22 +6,22 @@ import chat.rocket.android.core.behaviours.MessageView
interface ResetPasswordView : LoadingView, MessageView { interface ResetPasswordView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank email. * Shows a successful email sent message.
*/ */
fun alertBlankEmail() fun emailSent()
/** /**
* Alerts the user about a invalid email. * Shows a message to update the server version in order to use an app feature.
*/ */
fun alertInvalidEmail() fun updateYourServerVersion()
/** /**
* Shows a successful email sent message. * Enables the button to reset the password when the user inputs a valid email address.
*/ */
fun emailSent() fun enableButtonConnect()
/** /**
* Shows a message to update the server version in order to use an app feature. * Disables the button to reset the password when the user entered an invalid email address
*/ */
fun updateYourServerVersion() fun disableButtonConnect()
} }
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.ui package chat.rocket.android.authentication.resetpassword.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordPresenter import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordPresenter
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showKeyboard import chat.rocket.android.util.extensions.showKeyboard
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_authentication_reset_password.* import kotlinx.android.synthetic.main.fragment_authentication_reset_password.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
internal const val TAG_RESET_PASSWORD_FRAGMENT = "ResetPasswordFragment" fun newInstance(): Fragment = ResetPasswordFragment()
class ResetPasswordFragment : Fragment(), ResetPasswordView { class ResetPasswordFragment : Fragment(), ResetPasswordView {
@Inject @Inject
lateinit var presenter: ResetPasswordPresenter lateinit var presenter: ResetPasswordPresenter
@Inject @Inject
lateinit var analyticsManager: AnalyticsManager lateinit var analyticsManager: AnalyticsManager
private lateinit var emailAddressDisposable: Disposable
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -52,34 +55,39 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView { ...@@ -52,34 +55,39 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView {
showKeyboard(text_email) showKeyboard(text_email)
} }
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener() setupOnClickListener()
subscribeEditText()
analyticsManager.logScreenView(ScreenViewEvent.ResetPassword) analyticsManager.logScreenView(ScreenViewEvent.ResetPassword)
} }
override fun alertBlankEmail() { override fun onDestroyView() {
ui { super.onDestroyView()
vibrateShakeAndRequestFocusForTextEmail() unsubscribeEditText()
}
} }
override fun alertInvalidEmail() { override fun emailSent() = showMessage(R.string.msg_check_your_email_to_reset_your_password)
ui {
vibrateShakeAndRequestFocusForTextEmail() override fun updateYourServerVersion() =
showMessage(R.string.msg_invalid_email) showMessage(R.string.msg_update_app_version_in_order_to_continue)
}
}
override fun emailSent() { override fun enableButtonConnect() {
showToast(R.string.msg_check_your_email_to_reset_your_password, Toast.LENGTH_LONG) context?.let {
ViewCompat.setBackgroundTintList(
button_reset_password, ContextCompat.getColorStateList(it, R.color.colorAccent)
)
button_reset_password.isEnabled = true
}
} }
override fun updateYourServerVersion() { override fun disableButtonConnect() {
showMessage(R.string.msg_update_app_version_in_order_to_continue) context?.let {
ViewCompat.setBackgroundTintList(
button_reset_password,
ContextCompat.getColorStateList(it, R.color.colorAuthenticationButtonDisabled)
)
button_reset_password.isEnabled = false
}
} }
override fun showLoading() { override fun showLoading() {
...@@ -108,42 +116,35 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView { ...@@ -108,42 +116,35 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView {
} }
} }
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
showMessage(getString(R.string.msg_generic_error))
}
private fun tintEditTextDrawableStart() {
ui {
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
DrawableHelper.wrapDrawable(emailDrawable)
DrawableHelper.tintDrawable(emailDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_email, emailDrawable)
}
}
private fun enableUserInput() { private fun enableUserInput() {
button_reset_password.isEnabled = true enableButtonConnect()
text_email.isEnabled = true text_email.isEnabled = true
} }
private fun disableUserInput() { private fun disableUserInput() {
button_reset_password.isEnabled = false disableButtonConnect()
text_email.isEnabled = true text_email.isEnabled = false
}
private fun vibrateShakeAndRequestFocusForTextEmail() {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
} }
private fun setupOnClickListener() { private fun setupOnClickListener() =
button_reset_password.setOnClickListener { button_reset_password.setOnClickListener {
presenter.resetPassword(text_email.textContent) presenter.resetPassword(text_email.textContent)
} }
}
companion object { private fun subscribeEditText() {
fun newInstance() = ResetPasswordFragment() emailAddressDisposable = text_email.asObservable()
.debounce(300, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.filter { it.isNotBlank() }
.subscribe {
if (it.toString().isEmail()) {
enableButtonConnect()
} else {
disableButtonConnect()
}
}
} }
private fun unsubscribeEditText() = emailAddressDisposable.dispose()
} }
\ No newline at end of file
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.server.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.server.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.server.presentation.ServerView import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class ServerFragmentModule { class ServerFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun serverView(frag: ServerFragment): ServerView = frag
@Provides @Provides
@PerFragment @PerFragment
fun serverView(frag: ServerFragment): ServerView { fun provideLifecycleOwner(frag: ServerFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: ServerFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -5,12 +5,15 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator ...@@ -5,12 +5,15 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveConnectingServerInteractor import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.isValidUrl
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject import javax.inject.Inject
class ServerPresenter @Inject constructor( class ServerPresenter @Inject constructor(
...@@ -20,42 +23,86 @@ class ServerPresenter @Inject constructor( ...@@ -20,42 +23,86 @@ class ServerPresenter @Inject constructor(
private val serverInteractor: SaveConnectingServerInteractor, private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory val settingsInteractor: GetSettingsInteractor,
) : CheckServerPresenter(strategy, factory, view) { val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor, view) {
fun checkServer(server: String) { fun checkServer(server: String) {
if (!server.isValidUrl()) { if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage() view.showInvalidServerUrlMessage()
} else { } else {
view.showLoading() view.showLoading()
setupConnectionInfo(server)
checkServerInfo(server) checkServerInfo(server)
} }
} }
fun connect(server: String) { fun connect(serverUrl: String) {
//code that leads to login screen (smart lock will be implemented after this) connectToServer(serverUrl) {
connectToServer(server) { if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) {
navigator.toLogin() navigator.toLogin(serverUrl)
} else {
navigator.toLoginOptions(
serverUrl,
state,
facebookOauthUrl,
githubOauthUrl,
googleOauthUrl,
linkedinOauthUrl,
gitlabOauthUrl,
wordpressOauthUrl,
casLoginUrl,
casToken,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
customOauthServiceButtonColor,
samlUrl,
samlToken,
samlServiceName,
samlServiceNameTextColor,
samlServiceButtonColor,
totalSocialAccountsEnabled,
isLoginFormEnabled,
isNewAccountCreationEnabled
)
}
} }
} }
private fun connectToServer(server: String, block: () -> Unit) { fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
if (!server.isValidUrl()) { connectToServer(deepLinkInfo.url) {
navigator.toLoginOptions(deepLinkInfo.url, deepLinkInfo = deepLinkInfo)
}
}
private fun connectToServer(serverUrl: String, block: () -> Unit) {
if (!serverUrl.isValidUrl()) {
view.showInvalidServerUrlMessage() view.showInvalidServerUrlMessage()
} else { } else {
launchUI(strategy) { launchUI(strategy) {
// Check if we already have an account for this server... // Check if we already have an account for this server...
val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == server } val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == serverUrl }
if (account != null) { if (account != null) {
navigator.toChatList(server) navigator.toChatList(serverUrl)
return@launchUI return@launchUI
} }
view.showLoading() view.showLoading()
try { try {
refreshSettingsInteractor.refresh(server) withContext(DefaultDispatcher) {
serverInteractor.save(server) refreshSettingsInteractor.refresh(serverUrl)
block()
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it
checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled()
serverInteractor.save(serverUrl)
block()
}
} catch (ex: Exception) { } catch (ex: Exception) {
view.showMessage(ex) view.showMessage(ex)
} finally { } finally {
...@@ -65,10 +112,4 @@ class ServerPresenter @Inject constructor( ...@@ -65,10 +112,4 @@ class ServerPresenter @Inject constructor(
} }
} }
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
//code that leads to login screen (smart lock will be implemented after this)
connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo)
}
}
} }
\ No newline at end of file
...@@ -9,4 +9,15 @@ interface ServerView : LoadingView, MessageView, VersionCheckView { ...@@ -9,4 +9,15 @@ interface ServerView : LoadingView, MessageView, VersionCheckView {
* Shows an invalid server URL message. * Shows an invalid server URL message.
*/ */
fun showInvalidServerUrlMessage() fun showInvalidServerUrlMessage()
/**
* Enables the button to connect to the server when the user inputs a valid url.
*/
fun enableButtonConnect()
/**
* Disables the button to connect to the server when the server address entered by the user
* is not a valid url.
*/
fun disableButtonConnect()
} }
\ No newline at end of file
...@@ -3,6 +3,7 @@ package chat.rocket.android.authentication.server.presentation ...@@ -3,6 +3,7 @@ package chat.rocket.android.authentication.server.presentation
import okhttp3.HttpUrl import okhttp3.HttpUrl
interface VersionCheckView { interface VersionCheckView {
/** /**
* Alerts the user about the server version not meeting the recommended server version. * Alerts the user about the server version not meeting the recommended server version.
*/ */
......
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.signup.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.signup.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.signup.presentation.SignupView import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.authentication.signup.ui.SignupFragment import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class SignupFragmentModule { class SignupFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun signupView(frag: SignupFragment): SignupView = frag
@Provides @Provides
@PerFragment @PerFragment
fun signupView(frag: SignupFragment): SignupView { fun provideLifecycleOwner(frag: SignupFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: SignupFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -44,57 +44,37 @@ class SignupPresenter @Inject constructor( ...@@ -44,57 +44,37 @@ class SignupPresenter @Inject constructor(
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun signup(name: String, username: String, password: String, email: String) { fun signup(name: String, username: String, password: String, email: String) {
val server = serverInteractor.get() val client = factory.create(currentServer)
when { launchUI(strategy) {
server == null -> { view.showLoading()
navigator.toServerScreen() try {
} // TODO This function returns a user so should we save it?
name.isBlank() -> { retryIO("signup") { client.signup(email, name, username, password) }
view.alertBlankName() // TODO This function returns a user token so should we save it?
} retryIO("login") { client.login(username, password) }
username.isBlank() -> { val me = retryIO("me") { client.me() }
view.alertBlankUsername() saveCurrentServerInteractor.save(currentServer)
} localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
password.isEmpty() -> { saveAccount(me)
view.alertEmptyPassword() analyticsManager.logSignUp(
} AuthenticationEvent.AuthenticationWithUserAndPassword,
email.isBlank() -> { true
view.alertBlankEmail() )
} view.saveSmartLockCredentials(username, password)
else -> { navigator.toChatList()
val client = factory.create(server) } catch (exception: RocketChatException) {
launchUI(strategy) { analyticsManager.logSignUp(
view.showLoading() AuthenticationEvent.AuthenticationWithUserAndPassword,
try { false
// TODO This function returns a user so should we save it? )
retryIO("signup") { client.signup(email, name, username, password) } exception.message?.let {
// TODO This function returns a user token so should we save it? view.showMessage(it)
retryIO("login") { client.login(username, password) } }.ifNull {
val me = retryIO("me") { client.me() } view.showGenericErrorMessage()
saveCurrentServerInteractor.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
view.saveSmartLockCredentials(username, password)
navigator.toChatList()
} catch (exception: RocketChatException) {
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
} }
} finally {
view.hideLoading()
} }
} }
} }
......
...@@ -6,24 +6,15 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -6,24 +6,15 @@ import chat.rocket.android.core.behaviours.MessageView
interface SignupView : LoadingView, MessageView { interface SignupView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank name. * Enables the button to register when the user enters all the required fields.
*/ */
fun alertBlankName() fun enableButtonRegister()
/** /**
* Alerts the user about a blank username. * Disables the button to register when the user doesn't enter all the required fields.
*/ */
fun alertBlankUsername() fun disableButtonRegister()
/**
* Alerts the user about a empty password.
*/
fun alertEmptyPassword()
/**
* Alerts the user about a blank email.
*/
fun alertBlankEmail()
/** /**
* Saves Google Smart Lock credentials. * Saves Google Smart Lock credentials.
......
...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.twofactor.di ...@@ -3,34 +3,18 @@ package chat.rocket.android.authentication.twofactor.di
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class TwoFAFragmentModule { class TwoFAFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideJob() = Job() fun loginView(frag: TwoFAFragment): TwoFAView = frag
@Provides @Provides
@PerFragment @PerFragment
fun loginView(frag: TwoFAFragment): TwoFAView { fun provideLifecycleOwner(frag: TwoFAFragment): LifecycleOwner = frag
return frag
}
@Provides
@PerFragment
fun provideLifecycleOwner(frag: TwoFAFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
...@@ -35,77 +35,65 @@ class TwoFAPresenter @Inject constructor( ...@@ -35,77 +35,65 @@ class TwoFAPresenter @Inject constructor(
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor, private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val analyticsManager: AnalyticsManager, private val analyticsManager: AnalyticsManager,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
settingsInteractor: GetSettingsInteractor val serverInteractor: GetConnectingServerInteractor,
val settingsInteractor: GetSettingsInteractor
) { ) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
// TODO: If the usernameOrEmail and password was informed by the user on the previous screen, then we should pass only the pin, like this: fun authenticate(pin: EditText)
fun authenticate( fun authenticate(
usernameOrEmail: String, usernameOrEmail: String,
password: String, password: String,
twoFactorAuthenticationCode: String twoFactorAuthenticationCode: String
) { ) {
val server = serverInteractor.get() launchUI(strategy) {
when { val client = factory.create(currentServer)
server == null -> { view.showLoading()
navigator.toServerScreen() try {
} // The token is saved via the client TokenProvider
twoFactorAuthenticationCode.isBlank() -> { val token = retryIO("login") {
view.alertBlankTwoFactorAuthenticationCode() if (usernameOrEmail.isEmail()) {
} client.loginWithEmail(
else -> { usernameOrEmail,
launchUI(strategy) { password,
val client = factory.create(server) twoFactorAuthenticationCode
view.showLoading()
try {
// The token is saved via the client TokenProvider
val token = retryIO("login") {
if (usernameOrEmail.isEmail()) {
client.loginWithEmail(usernameOrEmail, password, twoFactorAuthenticationCode)
} else {
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
}
}
val me = retryIO("me") { client.me() }
saveAccount(me)
saveCurrentServerInteractor.save(currentServer)
tokenRepository.save(server, token)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
) )
navigator.toChatList() } else {
} catch (exception: RocketChatException) { client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
if (exception is RocketChatAuthException) {
view.alertInvalidTwoFactorAuthenticationCode()
} else {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
} finally {
view.hideLoading()
} }
} }
val me = retryIO("me") { client.me() }
saveAccount(me)
saveCurrentServerInteractor.save(currentServer)
tokenRepository.save(currentServer, token)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword, true
)
navigator.toChatList()
} catch (exception: RocketChatException) {
if (exception is RocketChatAuthException) {
view.alertInvalidTwoFactorAuthenticationCode()
} else {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword, false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
} finally {
view.hideLoading()
} }
} }
} }
fun signup() = navigator.toSignUp()
private suspend fun saveAccount(me: Myself) { private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let { val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it) currentServer.serverLogoUrl(it)
......
...@@ -6,9 +6,14 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -6,9 +6,14 @@ import chat.rocket.android.core.behaviours.MessageView
interface TwoFAView : LoadingView, MessageView { interface TwoFAView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank Two Factor Authentication code. * Enables the button to set the username if the user entered at least one character.
*/ */
fun alertBlankTwoFactorAuthenticationCode() fun enableButtonConfirm()
/**
* Disables the button to set the username when there is no character entered by the user.
*/
fun disableButtonConfirm()
/** /**
* Alerts the user about an invalid inputted Two Factor Authentication code. * Alerts the user about an invalid inputted Two Factor Authentication code.
......
...@@ -226,8 +226,10 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback ...@@ -226,8 +226,10 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
private fun setupRecyclerView() { private fun setupRecyclerView() {
ui { ui {
recycler_view.layoutManager = recycler_view.layoutManager =
LinearLayoutManager(context, RecyclerView.VERTICAL, false) LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(it, DividerItemDecoration.HORIZONTAL)) recycler_view.addItemDecoration(
DividerItemDecoration(it, DividerItemDecoration.HORIZONTAL)
)
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
} }
......
package chat.rocket.android.helper package chat.rocket.android.helper
import chat.rocket.android.util.extensions.encodeToBase64
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.removeTrailingSlash import chat.rocket.android.util.extensions.removeTrailingSlash
object OauthHelper { object OauthHelper {
/**
* Returns an unguessable random string used to protect against forgery attacks.
*/
fun getState() =
("{\"loginStyle\":\"popup\"," +
"\"credentialToken\":\"${generateRandomString(40)}\"," +
"\"isCordova\":true}").encodeToBase64()
/** /**
* Returns the Github Oauth URL. * Returns the Github Oauth URL.
* *
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment