Skip to content

Instantly share code, notes, and snippets.

@brendanw
Last active May 7, 2020 19:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brendanw/5772c37126c3b20c07a5a6c6e27ad6fd to your computer and use it in GitHub Desktop.
Save brendanw/5772c37126c3b20c07a5a6c6e27ad6fd to your computer and use it in GitHub Desktop.
open class BaseViewModel<T : Any, R : Any>(
initialState: T
) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
protected val _state = ConflatedBroadcastChannel<T>(initialState)
val state = _state.asFlow()
val cState = state.wrap()
protected val _effect = Channel<R>()
val effect = _effect.consumeAsFlow().wrap()
val cEffect = effect.wrap()
open fun onClear() {
job.cancel()
}
fun updateState(action: T.() -> T) {
launch {
_state.update { action() }
}
}
}
suspend fun <T> ConflatedBroadcastChannel<T>.update(block: T.() -> T) {
val newValue = value.block()
send(newValue)
}
class EnterCredentialsContract {
enum class StateType {
DEFAULT,
SHOW_LOADER
}
data class State(
val type: StateType = DEFAULT,
val showLoader: Boolean = false
)
enum class Effect {
LaunchHome,
LaunchConfirmToken,
LaunchSubscribeInterstitial,
LaunchForgotPassword,
ShowInvalidUsernameOrPasswordError,
ShowInvalidEmailError
}
}
class EnterCredentialsFragment : Fragment(), LoginFragment {
private lateinit var viewModel: EnterCredentialsViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auth_enter_creds, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = EnterCredentialsViewModel()
setup()
lifecycleScope.launch { viewModel.state.collect { render(it) } }
lifecycleScope.launch {
viewModel.effect.collect { effect ->
when (effect) {
LaunchHome -> {
val intent = Intent(view.context, MapActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
view.context.startActivity(intent)
}
LaunchConfirmToken -> {
val intent = Intent(view.context, CreateAccountActivity::class.java)
intent.putExtra(CreateAccountActivity.KEY_OPEN_VERIFY, true)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
view.context.startActivity(intent)
}
LaunchSubscribeInterstitial -> {
val intent = Intent(view.context, CreateAccountActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(CreateAccountActivity.KEY_OPEN_SUBSCRIBE, true)
view.context.startActivity(intent)
}
LaunchForgotPassword -> {
loginActivity().openFragment(ForgotPasswordFragment.newInstance())
}
ShowInvalidUsernameOrPasswordError -> {
view.showErrorDialog("Sign In Error", "Invalid username or password")
}
ShowInvalidEmailError -> {
view.showErrorDialog("Sign In Error", getString(R.string.sign_up_error_username))
}
}.safe
}
}
}
private fun render(state: EnterCredentialsContract.State) {
enter_creds_loader.visibleIf(state.showLoader)
}
override fun onDestroy() {
super.onDestroy()
viewModel.onClear()
}
private fun setup() {
sign_in_button.setOnClickListener {
viewModel.tapSignIn(username.text.toString(), password.text.toString())
}
forgot_password.setOnClickListener {
viewModel.tapForgotPassword()
}
}
companion object {
fun newInstance() = EnterCredentialsFragment()
}
}
class EnterCredentialsViewController: AuthView {
var viewModel: EnterCredentialsViewModel? = nil
var email = String()
var password = String()
var signInResponse: SignInResponse?
var emailTextField: UITextField!
var passwordTextField: UITextField!
var signInButton: UIButton!
deinit {
viewModel?.onClear()
viewModel = nil
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel = EnterCredentialsViewModelKt.createEnterCredentialsVM()
setupUI()
viewModel?.cState.watch { [unowned self] state in
self.render(state: state!)
}
viewModel?.cEffect.watch { [unowned self] effect in
guard let effect = effect else { return }
switch(effect) {
case .launchhome:
AppDelegate.shared.rootViewController.switchToHome()
case .launchconfirmtoken:
let new = ConfirmTokenViewController()
self.navigationController?.pushViewController(new, animated: true)
case .launchforgotpassword:
let new = ForgotPasswordController()
self.navigationController?.pushViewController(new, animated: true)
case .launchsubscribeinterstitial:
self.present(UINavigationController(rootViewController: InterstitialViewController()), animated: true, completion: nil)
case .showinvalidusernameorpassworderror:
self.showAlert("Please use a valid email or password.")
case .showinvalidemailerror:
self.showAlert("Please use a valid email address")
default:
print("should not get here")
}
}
}
private func render(state: EnterCredentialsContract.State) {
if (state.showLoader) {
showSpinner(onView: view)
signInButton.isEnabled = false
} else {
self.removeSpinner()
self.signInButton.isEnabled = true
}
}
private func setupUI() {
// initialize views and position
}
override open var prefersStatusBarHidden: Bool {
return true
}
@objc func forgotBtnTapped(_ sender: UIButton) {
self.viewModel?.tapForgotPassword()
}
@objc func createAccountTapped(_ sender: UIButton) {
navigationController?.pushViewController(NameViewController(), animated: true)
}
func loginSuccess() {
self.removeSpinner()
self.signInButton.isEnabled = true
}
@objc func loginTapped(_ sender: UIButton) {
email = String(emailTextField.text!)
password = String(passwordTextField.text!)
showSpinner(onView: view)
signInButton.isEnabled = false
viewModel?.tapSignIn(username: email, password: password)
}
}
class EnterCredentialsViewModel(
private val userRepository: UserRepository = AppDependencies.userRepository,
private val analytics: Analytics = AppDependencies.analytics
) : BaseBetaViewModel<State, Effect>(State()) {
init {
analytics.trackEvent(AE.VIEW_SIGNIN)
}
fun tapForgotPassword() {
launch { _effect.send(LaunchForgotPassword) }
}
fun tapSignIn(username: String, password: String) {
launch {
if (!username.matches(emailRegex)) {
_effect.send(ShowInvalidEmailError)
return@launch
}
_state.update { copy(type = SHOW_LOADER, showLoader = true) }
userRepository.signIn(username, password)
.flowOn(ioDispatcher())
.collect { response ->
if (response.success) {
//Amplitude.getInstance().userId = signInResponse._id
// val map = mapOf("email" to signInResponse.email)
//Amplitude.getInstance().setUserProperties(JSONObject(map))
routeToNextTask(response)
} else {
_state.update { copy(showLoader = false) }
_effect.send(ShowInvalidUsernameOrPasswordError)
}
}
}
}
private suspend fun routeToNextTask(signInResponse: SignInResponse) {
if (signInResponse.isVerified) {
if (signInResponse.subscription != null && signInResponse.subscription.isSubscribed) {
_effect.send(LaunchHome)
} else {
_effect.send(LaunchSubscribeInterstitial)
}
} else {
_effect.send(LaunchConfirmToken)
}
}
}
fun createEnterCredentialsVM(): EnterCredentialsViewModel {
return EnterCredentialsViewModel()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment