Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/flipcash/shared/authentication/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies {
implementation(project(":apps:flipcash:shared:push"))
implementation(project(":apps:flipcash:shared:featureflags"))
implementation(project(":apps:flipcash:shared:tokens"))
implementation(project(":apps:flipcash:shared:profile"))
implementation(project(":apps:flipcash:shared:userflags"))
implementation(project(":services:flipcash"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import com.flipcash.app.persistence.PersistenceProvider
import com.flipcash.app.push.PushTokenProvider
import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.shared.profile.ProfileCoordinator
import com.flipcash.services.controllers.AccountController
import com.flipcash.services.controllers.ProfileController
import com.flipcash.services.controllers.PushController
import com.flipcash.services.user.AuthState
import com.flipcash.services.user.UserManager
import com.flipcash.shared.authentication.BuildConfig
import com.getcode.crypt.MnemonicPhrase
import com.getcode.opencode.controllers.TokenController
import com.getcode.opencode.model.core.ID
import com.getcode.utils.TraceManager
import com.getcode.utils.TraceType
Expand Down Expand Up @@ -46,9 +46,10 @@ class AuthManager @Inject constructor(
private val pushTokenProvider: PushTokenProvider,
private val tokenCoordinator: TokenCoordinator,
private val persistence: PersistenceProvider,
private val featureFlagController: FeatureFlagController,
private val featureFlags: FeatureFlagController,
private val appSettings: AppSettingsCoordinator,
private val userFlags: UserFlagsCoordinator,
private val profileCoordinator: ProfileCoordinator,
private val contactCoordinator: ContactCoordinator,
private val networkObserver: NetworkConnectivityListener,
private val dispatchers: DispatcherProvider,
Expand Down Expand Up @@ -194,6 +195,9 @@ class AuthManager @Inject constructor(

coroutineScope {
launch {
profileCoordinator.restore()
userFlags.restoreFlags()

val flags = if (!isSoftLogin || networkObserver.isConnected) {
retryable(maxRetries = 3) {
accountController.getUserFlags().getOrNull()
Expand Down Expand Up @@ -292,9 +296,11 @@ class AuthManager @Inject constructor(
userManager.clear()
tokenCoordinator.reset()
persistence.close()
featureFlagController.reset()
featureFlags.reset()
appSettings.reset()
userFlags.clearAll()
profileCoordinator.reset()
userFlags.resetCache()

if (!BuildConfig.DEBUG) TraceManager.userId = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.flipcash.app.persistence.PersistenceProvider
import com.flipcash.app.push.PushTokenProvider
import com.flipcash.app.tokens.TokenCoordinator
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.shared.profile.ProfileCoordinator
import com.flipcash.services.controllers.AccountController
import com.flipcash.services.controllers.ProfileController
import com.flipcash.services.controllers.PushController
Expand Down Expand Up @@ -59,6 +60,7 @@ class AuthManagerTest {
private val featureFlagController: FeatureFlagController = mockk(relaxed = true)
private val appSettings: AppSettingsCoordinator = mockk(relaxed = true)
private val userFlags: UserFlagsCoordinator = mockk(relaxed = true)
private val profileCoordinator: ProfileCoordinator = mockk(relaxed = true)
private val contactCoordinator: ContactCoordinator = mockk(relaxed = true)
private val userManagerState = MutableStateFlow(UserManager.State())

Expand Down Expand Up @@ -92,9 +94,10 @@ class AuthManagerTest {
pushTokenProvider = pushTokenProvider,
tokenCoordinator = tokenCoordinator,
persistence = persistence,
featureFlagController = featureFlagController,
featureFlags = featureFlagController,
appSettings = appSettings,
userFlags = userFlags,
profileCoordinator = profileCoordinator,
contactCoordinator = contactCoordinator,
dispatchers = dispatchers,
networkObserver = networkConnectivityListener,
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/shared/profile/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
17 changes: 17 additions & 0 deletions apps/flipcash/shared/profile/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.flipcash.android.library)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "${Gradle.flipcashNamespace}.shared.profile"
}

dependencies {
implementation(libs.bundles.hilt)
implementation(libs.androidx.datastore)
implementation(libs.bundles.kotlinx.serialization)

implementation(project(":libs:coroutines"))
implementation(project(":services:flipcash"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.flipcash.shared.profile

import android.content.Context
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import com.flipcash.libs.coroutines.DispatcherProvider
import com.flipcash.services.models.SocialAccount
import com.flipcash.services.models.UserProfile
import com.flipcash.services.user.UserManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ProfileCoordinator @Inject constructor(
@param:ApplicationContext private val context: Context,
private val userManager: UserManager,
dispatchers: DispatcherProvider,
) {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.IO)

private val dataStore = PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() },
scope = scope,
produceFile = { context.preferencesDataStoreFile("user-profile") }
)

private val json = Json { ignoreUnknownKeys = true }

init {
scope.launch {
userManager.state
.map { it.userProfile }
.filterNotNull()
.distinctUntilChanged()
.collect { profile -> persist(profile) }
}
}

suspend fun restore() {
val prefs = dataStore.data.first()
val raw = prefs[KEY_PROFILE] ?: return
val cached = runCatching { json.decodeFromString<CachedProfile>(raw) }.getOrNull() ?: return
userManager.set(cached.toDomain())
}

suspend fun reset() {
dataStore.edit { it.remove(KEY_PROFILE) }
}

private suspend fun persist(profile: UserProfile) {
val cached = CachedProfile.fromDomain(profile)
val raw = json.encodeToString(cached)
dataStore.edit { it[KEY_PROFILE] = raw }
}

companion object {
private val KEY_PROFILE = stringPreferencesKey("cached_user_profile")
}
}

@Serializable
private data class CachedProfile(
val displayName: String? = null,
val socialAccounts: List<CachedSocialAccount> = emptyList(),
val verifiedPhoneNumber: String? = null,
val verifiedEmailAddress: String? = null,
) {
fun toDomain(): UserProfile = UserProfile(
displayName = displayName,
socialAccounts = socialAccounts.mapNotNull { it.toDomain() },
verifiedPhoneNumber = verifiedPhoneNumber,
verifiedEmailAddress = verifiedEmailAddress,
)

companion object {
fun fromDomain(profile: UserProfile): CachedProfile = CachedProfile(
displayName = profile.displayName,
socialAccounts = profile.socialAccounts.map { CachedSocialAccount.fromDomain(it) },
verifiedPhoneNumber = profile.verifiedPhoneNumber,
verifiedEmailAddress = profile.verifiedEmailAddress,
)
}
}

@Serializable
private sealed interface CachedSocialAccount {
@Serializable
@SerialName("twitter_x")
data class TwitterX(
val id: String,
val username: String,
val name: String,
val description: String,
val profilePicUrl: String,
val verifiedType: String? = null,
val followerCount: Int,
) : CachedSocialAccount

fun toDomain(): SocialAccount? = when (this) {
is TwitterX -> SocialAccount.TwitterX(
id = id,
username = username,
name = name,
description = description,
profilePicUrl = profilePicUrl,
verifiedType = verifiedType?.let {
runCatching { SocialAccount.TwitterX.VerifiedType.valueOf(it) }.getOrNull()
},
followerCount = followerCount,
)
}

companion object {
fun fromDomain(account: SocialAccount): CachedSocialAccount = when (account) {
is SocialAccount.TwitterX -> TwitterX(
id = account.id,
username = account.username,
name = account.name,
description = account.description,
profilePicUrl = account.profilePicUrl,
verifiedType = account.verifiedType?.name,
followerCount = account.followerCount,
)
}
}
}
2 changes: 2 additions & 0 deletions apps/flipcash/shared/userflags/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.flipcash.android.library.compose)
alias(libs.plugins.kotlin.serialization)
}

android {
Expand All @@ -11,6 +12,7 @@ dependencies {
implementation(libs.androidx.datastore)
implementation(libs.compose.foundation)
implementation(libs.compose.ui.text)
implementation(libs.kotlinx.serialization.json)

implementation(project(":libs:coroutines"))
implementation(project(":libs:datetime"))
Expand Down
Loading
Loading