Skip to content

Commit

Permalink
Fix #39 Add account feature and login
Browse files Browse the repository at this point in the history
  • Loading branch information
herrbert74 committed Dec 24, 2024
1 parent e604bff commit 46ddbc8
Show file tree
Hide file tree
Showing 40 changed files with 1,383 additions and 2 deletions.
1 change: 1 addition & 0 deletions account/account-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
40 changes: 40 additions & 0 deletions account/account-data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.detekt)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.google.dagger.hilt.android)
alias(libs.plugins.ksp)
alias(libs.plugins.serialization)
id("android-library-convention")
id("data-convention")
}

apply(from = project.rootProject.file("config/detekt/detekt.gradle"))

android {
namespace = "com.zsoltbertalan.flickslate.account.data"

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
@Suppress("UnstableApiUsage")
testFixtures {
enable = true
}
}

dependencies {
api(project(":account:account-domain"))
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.squareUp.okhttp3.mockWebServer)

//Remove in AGP 8.9.0 https://issuetracker.google.com/issues/340315591
testFixturesCompileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.0")
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.add("-opt-in=okhttp3.ExperimentalOkHttpApi")
}
Empty file.
21 changes: 21 additions & 0 deletions account/account-data/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions account/account-data/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.zsoltbertalan.flickslate.account.data.api

import com.zsoltbertalan.flickslate.shared.model.Account
import com.zsoltbertalan.flickslate.shared.util.Outcome

interface AccountDataSource {

interface Local {

fun saveAccessToken(accessToken: String)
fun getAccessToken(): String?

fun saveAccount(account: Account)
fun getAccount(): Account?

}

interface Remote {

suspend fun createSessionId(username: String, password: String): Outcome<String>

suspend fun getAccountDetails(sessionToken: String): Outcome<Account>

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.zsoltbertalan.flickslate.account.data.local

import android.content.Context
import com.zsoltbertalan.flickslate.account.data.api.AccountDataSource
import com.zsoltbertalan.flickslate.shared.model.Account
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
import se.ansman.dagger.auto.AutoBind
import javax.inject.Inject

@AutoBind
@ActivityRetainedScoped
class AccountLocalDataSource @Inject constructor(
@ApplicationContext val context: Context
) : AccountDataSource.Local {

override fun saveAccessToken(accessToken: String) {
val sharedPreferences = context.getSharedPreferences("Account", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("access_token", accessToken).apply()
}

override fun getAccessToken(): String? {
val sharedPreferences = context.getSharedPreferences("Account", Context.MODE_PRIVATE)
return sharedPreferences.getString("access_token", null)
}

override fun saveAccount(account: Account) {
val sharedPreferences = context.getSharedPreferences("Account", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("account_name", account.name).apply()
}

override fun getAccount(): Account? {
val sharedPreferences = context.getSharedPreferences("Account", Context.MODE_PRIVATE)
return sharedPreferences.getString("account_name", null)?.let { Account(it) }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.zsoltbertalan.flickslate.account.data.network

import com.github.michaelbull.result.andThen
import com.github.michaelbull.result.map
import com.zsoltbertalan.flickslate.account.data.api.AccountDataSource
import com.zsoltbertalan.flickslate.shared.data.util.runCatchingApi
import com.zsoltbertalan.flickslate.shared.model.Account
import com.zsoltbertalan.flickslate.shared.util.Outcome
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import se.ansman.dagger.auto.AutoBind
import javax.inject.Inject

@AutoBind
@ActivityRetainedScoped
class AccountRemoteDataSource @Inject constructor(
private val accountService: AccountService
) : AccountDataSource.Remote {

override suspend fun createSessionId(username: String, password: String): Outcome<String> {
return runCatchingApi {
accountService.createRequestToken()
}.andThen { createRequestTokenReplyDto ->

runCatchingApi {
accountService.validateRequestTokenWithLogin(
createValidateRequestTokenWithLoginRequestBody(
username,
password,
createRequestTokenReplyDto.request_token
)
)
}
}.andThen { createRequestTokenReplyDto ->
runCatchingApi {
accountService.createSession(createRequestTokenReplyDto.request_token).session_id
}
}
}

override suspend fun getAccountDetails(sessionToken: String): Outcome<Account> {
return runCatchingApi {
accountService.getAccountDetails(sessionToken)
}.map { accountDetailsReplyDto ->
val accountName =
if (accountDetailsReplyDto.name.isNullOrEmpty()) accountDetailsReplyDto.username
else accountDetailsReplyDto.name
Account(accountName)
}
}

}

private fun createValidateRequestTokenWithLoginRequestBody(
username: String,
password: String,
requestToken: String,
): RequestBody {
val body = buildJsonObject {
put("username", username)
put("password", password)
put("request_token", requestToken)
}
return body.toString().toRequestBody("application/json; charset=UTF-8".toMediaType())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.zsoltbertalan.flickslate.account.data.network

import com.zsoltbertalan.flickslate.account.data.network.model.AccountDetailsReplyDto
import com.zsoltbertalan.flickslate.account.data.network.model.CreateRequestTokenReplyDto
import com.zsoltbertalan.flickslate.account.data.network.model.CreateSessionReplyDto
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

const val URL_CREATE_REQUEST_TOKEN = "authentication/token/new"
const val URL_VALIDATE_REQUEST_TOKEN_WITH_LOGIN = "authentication/token/validate_with_login"
const val URL_CREATE_SESSION = "authentication/session/new"
const val URL_GET_ACCOUNT_DETAILS = "account"

/**
* As of December 2024, the API reference for login with password is wrong here:
* https://developer.themoviedb.org/reference/authentication-how-do-i-generate-a-session-id
*
* But inspecting this example gives you the correct login flow:
* http://dev.travisbell.com/play/v3_auth_password.html
*
* The login replaces not the third, but the second step in the process, and you still have to create the session,
* because the second step is only validation, not the session creation.
*/
interface AccountService {

@GET(URL_CREATE_REQUEST_TOKEN)
suspend fun createRequestToken(): CreateRequestTokenReplyDto

@POST(URL_VALIDATE_REQUEST_TOKEN_WITH_LOGIN)
suspend fun validateRequestTokenWithLogin(
@Body requestBody: RequestBody
): CreateRequestTokenReplyDto

@GET(URL_CREATE_SESSION)
suspend fun createSession(
@Query("request_token") requestToken: String,
): CreateSessionReplyDto

@GET(URL_GET_ACCOUNT_DETAILS)
suspend fun getAccountDetails(
@Query("session_id") sessionToken: String,
): AccountDetailsReplyDto

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.zsoltbertalan.flickslate.account.data.network

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped
import retrofit2.Retrofit

@Module
@InstallIn(ActivityRetainedComponent::class)
class MoviesServiceModule {

@Provides
@ActivityRetainedScoped
internal fun provideMoviesService(retroFit: Retrofit): AccountService {
return retroFit.create(AccountService::class.java)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.zsoltbertalan.flickslate.account.data.network.model

import kotlinx.serialization.Serializable

@Suppress("ConstructorParameterNaming")
@Serializable
data class AccountDetailsReplyDto(val name: String?, val username: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.zsoltbertalan.flickslate.account.data.network.model

import kotlinx.serialization.Serializable

@Suppress("ConstructorParameterNaming", "PropertyName")
@Serializable
data class CreateRequestTokenReplyDto(val success: Boolean, val expires_at: String, val request_token: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.zsoltbertalan.flickslate.account.data.network.model

import kotlinx.serialization.Serializable

@Suppress("ConstructorParameterNaming", "PropertyName")
@Serializable
data class CreateSessionReplyDto(val success: Boolean, val session_id: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.zsoltbertalan.flickslate.account.data.repository

import com.github.michaelbull.result.andThen
import com.zsoltbertalan.flickslate.account.data.api.AccountDataSource
import com.zsoltbertalan.flickslate.account.domain.api.AccountRepository
import com.zsoltbertalan.flickslate.shared.model.Account
import com.zsoltbertalan.flickslate.shared.util.Outcome
import dagger.hilt.android.scopes.ActivityRetainedScoped
import se.ansman.dagger.auto.AutoBind
import javax.inject.Inject

@AutoBind
@ActivityRetainedScoped
class AccountAccessor @Inject constructor(
private val accountRemoteDataSource: AccountDataSource.Remote,
private val accountLocalDataSource: AccountDataSource.Local
) : AccountRepository {

override suspend fun getAccount(): Account? {
return accountLocalDataSource.getAccount()
}

override suspend fun login(username: String, password: String): Outcome<Account> {
return accountRemoteDataSource.createSessionId(username, password)
.andThen {
val account = accountRemoteDataSource.getAccountDetails(it)
accountLocalDataSource.saveAccount(account.value)
account
}
}

override fun logout() {
// Do nothing
}
}
1 change: 1 addition & 0 deletions account/account-domain/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
25 changes: 25 additions & 0 deletions account/account-domain/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id("android-library-convention")
alias(libs.plugins.serialization)
}

android {
namespace = "com.zsoltbertalan.flickslate.account.domain"
buildFeatures {
@Suppress("UnstableApiUsage")
testFixtures {
enable = true
}
}
}

dependencies {

api(project(":shared"))

implementation(libs.kotlinResult.result)
implementation(libs.kotlinx.collections.immutable.jvm)
implementation(libs.kotlinx.coroutines.core)
testFixturesCompileOnly(libs.kotlinx.collections.immutable.jvm)

}
Empty file.
21 changes: 21 additions & 0 deletions account/account-domain/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions account/account-domain/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Loading

0 comments on commit 46ddbc8

Please sign in to comment.