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
6 changes: 3 additions & 3 deletions Diary/Diary.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.2;
MARKETING_VERSION = 1.8.3;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.2;
MARKETING_VERSION = 1.8.3;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -518,7 +518,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8.2;
MARKETING_VERSION = 1.8.3;
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
62 changes: 31 additions & 31 deletions app/android/dependencies/realReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,28 @@ androidx.compose.material:material-icons-extended-android:1.7.6
androidx.compose.material:material-icons-extended:1.7.6
androidx.compose.material:material-ripple-android:1.11.0-rc01
androidx.compose.material:material-ripple:1.11.0-rc01
androidx.compose.runtime:runtime-android:1.11.0
androidx.compose.runtime:runtime-annotation-android:1.11.0
androidx.compose.runtime:runtime-annotation:1.11.0
androidx.compose.runtime:runtime-retain-android:1.11.0
androidx.compose.runtime:runtime-retain:1.11.0
androidx.compose.runtime:runtime-saveable-android:1.11.0
androidx.compose.runtime:runtime-saveable:1.11.0
androidx.compose.runtime:runtime:1.11.0
androidx.compose.ui:ui-android:1.11.0
androidx.compose.ui:ui-geometry-android:1.11.0
androidx.compose.ui:ui-geometry:1.11.0
androidx.compose.ui:ui-graphics-android:1.11.0
androidx.compose.ui:ui-graphics:1.11.0
androidx.compose.ui:ui-text-android:1.11.0
androidx.compose.ui:ui-text:1.11.0
androidx.compose.ui:ui-tooling-preview-android:1.11.0
androidx.compose.ui:ui-tooling-preview:1.11.0
androidx.compose.ui:ui-unit-android:1.11.0
androidx.compose.ui:ui-unit:1.11.0
androidx.compose.ui:ui-util-android:1.11.0
androidx.compose.ui:ui-util:1.11.0
androidx.compose.ui:ui:1.11.0
androidx.compose.runtime:runtime-android:1.11.1
androidx.compose.runtime:runtime-annotation-android:1.11.1
androidx.compose.runtime:runtime-annotation:1.11.1
androidx.compose.runtime:runtime-retain-android:1.11.1
androidx.compose.runtime:runtime-retain:1.11.1
androidx.compose.runtime:runtime-saveable-android:1.11.1
androidx.compose.runtime:runtime-saveable:1.11.1
androidx.compose.runtime:runtime:1.11.1
androidx.compose.ui:ui-android:1.11.1
androidx.compose.ui:ui-geometry-android:1.11.1
androidx.compose.ui:ui-geometry:1.11.1
androidx.compose.ui:ui-graphics-android:1.11.1
androidx.compose.ui:ui-graphics:1.11.1
androidx.compose.ui:ui-text-android:1.11.1
androidx.compose.ui:ui-text:1.11.1
androidx.compose.ui:ui-tooling-preview-android:1.11.1
androidx.compose.ui:ui-tooling-preview:1.11.1
androidx.compose.ui:ui-unit-android:1.11.1
androidx.compose.ui:ui-unit:1.11.1
androidx.compose.ui:ui-util-android:1.11.1
androidx.compose.ui:ui-util:1.11.1
androidx.compose.ui:ui:1.11.1
androidx.concurrent:concurrent-futures-ktx:1.1.0
androidx.concurrent:concurrent-futures:1.1.0
androidx.core:core-ktx:1.18.0
Expand Down Expand Up @@ -353,15 +353,15 @@ org.jetbrains.compose.material3:material3:1.11.0-alpha07
org.jetbrains.compose.material:material-icons-core:1.7.3
org.jetbrains.compose.material:material-icons-extended:1.7.3
org.jetbrains.compose.material:material-ripple:1.11.0-beta03
org.jetbrains.compose.runtime:runtime-saveable:1.11.0-rc01
org.jetbrains.compose.runtime:runtime:1.11.0-rc01
org.jetbrains.compose.ui:ui-geometry:1.11.0-rc01
org.jetbrains.compose.ui:ui-graphics:1.11.0-rc01
org.jetbrains.compose.ui:ui-text:1.11.0-rc01
org.jetbrains.compose.ui:ui-tooling-preview:1.11.0-rc01
org.jetbrains.compose.ui:ui-unit:1.11.0-rc01
org.jetbrains.compose.ui:ui-util:1.11.0-rc01
org.jetbrains.compose.ui:ui:1.11.0-rc01
org.jetbrains.compose.runtime:runtime-saveable:1.11.0
org.jetbrains.compose.runtime:runtime:1.11.0
org.jetbrains.compose.ui:ui-geometry:1.11.0
org.jetbrains.compose.ui:ui-graphics:1.11.0
org.jetbrains.compose.ui:ui-text:1.11.0
org.jetbrains.compose.ui:ui-tooling-preview:1.11.0
org.jetbrains.compose.ui:ui-unit:1.11.0
org.jetbrains.compose.ui:ui-util:1.11.0
org.jetbrains.compose.ui:ui:1.11.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.3.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22
Expand Down
4 changes: 2 additions & 2 deletions build-logic/src/main/kotlin/BuildConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ public data object BuildConfig {
internal const val ANDROID_TARGET_SDK = 36

public const val NAMESPACE: String = "io.github.taetae98coding.diary"
public const val VERSION_NAME: String = "1.8.2"
public const val VERSION_CODE: Int = 11
public const val VERSION_NAME: String = "1.8.3"
public const val VERSION_CODE: Int = 12
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import kotlin.uuid.Uuid

public sealed interface Account {
public val accountId: Uuid
public val isAuthorized: Boolean

public data object Guest : Account {
override val accountId: Uuid = Uuid.NIL
override val isAuthorized: Boolean = true
}

public data class User(
override val accountId: Uuid,
val email: String,
val profileImage: String?,
) : Account
val accountInfo: AccountInfo,
val accountMetaData: AccountMetaData?,
) : Account {
override val accountId: Uuid
get() = accountInfo.id

override val isAuthorized: Boolean
get() = accountInfo.isAuthorized
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import kotlin.uuid.Uuid
public data class AccountInfo(
val id: Uuid,
val email: String,
val isAuthorized: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package io.github.taetae98coding.diary.core.model.sync
public sealed interface SyncStatus {
public data object Idle : SyncStatus
public data class Syncing(val type: SyncType) : SyncStatus
public data object Failed : SyncStatus
public data class Failed(val type: SyncType) : SyncStatus
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ public sealed interface SupabaseSessionStatus {
val email: String?,
) : SupabaseSessionStatus

public data object NotAuthenticated : SupabaseSessionStatus
public data class NotAuthenticated(
val userId: String?,
val email: String?,
) : SupabaseSessionStatus {
public companion object {
public val NotLogin: NotAuthenticated = NotAuthenticated(userId = null, email = null)
}
}

public data object Loading : SupabaseSessionStatus
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase
)
}

is SessionStatus.NotAuthenticated -> SupabaseSessionStatus.NotAuthenticated
is SessionStatus.NotAuthenticated -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated.NotLogin

is SessionStatus.Initializing -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.Loading

Expand All @@ -40,17 +40,17 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase
}
}

private suspend fun resolveRefreshFailure(cause: RefreshFailureCause?): SupabaseSessionStatus {
private suspend fun resolveRefreshFailure(cause: RefreshFailureCause?): SupabaseSessionStatus.NotAuthenticated {
return when (cause) {
is RefreshFailureCause.InternalServerError -> SupabaseSessionStatus.NotAuthenticated
else -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated
is RefreshFailureCause.InternalServerError -> SupabaseSessionStatus.NotAuthenticated.NotLogin
else -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated.NotLogin
}
}

private suspend fun getSessionStatusFromStorage(): SupabaseSessionStatus? {
private suspend fun getSessionStatusFromStorage(): SupabaseSessionStatus.NotAuthenticated? {
return runCatching {
supabase.auth.sessionManager.loadSession().user
?.let { SupabaseSessionStatus.Authenticated(userId = it.id, email = it.email) }
?.let { SupabaseSessionStatus.NotAuthenticated(userId = it.id, email = it.email) }
}.getOrNull()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,21 @@ internal class AccountInfoRepositoryImpl(
status is SupabaseSessionStatus.Authenticated || status is SupabaseSessionStatus.NotAuthenticated
}.mapLatest { status ->
when (status) {
is SupabaseSessionStatus.Authenticated -> AccountInfo(
id = Uuid.parse(status.userId),
email = requireNotNull(status.email),
)
is SupabaseSessionStatus.Authenticated -> {
AccountInfo(
id = Uuid.parse(status.userId),
email = status.email ?: return@mapLatest null,
isAuthorized = true,
)
}

is SupabaseSessionStatus.NotAuthenticated -> null
is SupabaseSessionStatus.NotAuthenticated -> {
AccountInfo(
id = Uuid.parse(status.userId ?: return@mapLatest null),
email = status.email ?: return@mapLatest null,
isAuthorized = false,
)
}

else -> null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalCoroutinesApi::class)

package io.github.taetae98coding.diary.data.sync

import android.content.Context
Expand All @@ -13,10 +15,9 @@ import io.github.taetae98coding.diary.core.model.sync.SyncStatus
import io.github.taetae98coding.diary.core.model.sync.SyncType
import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase
import io.github.taetae98coding.diary.domain.sync.manager.SyncManager
import kotlin.time.Clock
import kotlin.uuid.Uuid
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
Expand All @@ -25,28 +26,32 @@ import org.koin.core.annotation.Single
@Single
internal class AndroidSyncManager(
private val context: Context,
private val clock: Clock,
getAccountUseCase: GetAccountUseCase,
) : SyncManager {
private val syncTypeFlow = MutableStateFlow(SyncType.Background)
override val syncStatus: Flow<SyncStatus> = combine(
syncTypeFlow,
getAccountUseCase(),
) { syncType, accountResult ->
override val syncStatus = getAccountUseCase().flatMapLatest { accountResult ->
accountResult.fold(
onSuccess = { account ->
WorkManager.getInstance(context).getWorkInfosForUniqueWorkFlow("SYNC_${account.accountId}")
.mapLatest { workInfos ->
when {
workInfos.any { it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED } -> SyncStatus.Syncing(syncType)
workInfos.any { it.state == WorkInfo.State.FAILED } -> SyncStatus.Failed
val last = workInfos.maxByOrNull { info ->
info.tags.find { it.startsWith("timestamp") }
?.removePrefix("timestamp")
?.toLongOrNull()
?: 0L
} ?: return@mapLatest SyncStatus.Idle

val syncType = SyncType.entries.find { last.tags.contains(it.name) } ?: SyncType.Background

when (last.state) {
WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> SyncStatus.Syncing(syncType)
WorkInfo.State.FAILED -> SyncStatus.Failed(syncType)
else -> SyncStatus.Idle
}
}
},
onFailure = { flowOf(SyncStatus.Idle) },
)
}.flatMapLatest {
it
}

override fun requestSync(
Expand All @@ -61,9 +66,10 @@ internal class AndroidSyncManager(
.setConstraints(constraints)
.setInputData(workDataOf(SyncWorker.ACCOUNT_ID to accountId.toString()))
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag("timestamp${clock.now().toEpochMilliseconds()}")
.addTag(type.name)
.build()

syncTypeFlow.value = type
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName = "SYNC_$accountId",
existingWorkPolicy = ExistingWorkPolicy.REPLACE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ internal class SyncWorker(

companion object {
const val ACCOUNT_ID = "accountId"
const val SYNC_TYPE = "syncType"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal class NonAndroidSyncManager(
throwable = throwable,
),
)
_syncStatus.value = SyncStatus.Failed
_syncStatus.value = SyncStatus.Failed(type)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ public class GetAccountUseCase(
Account.Guest
} else {
Account.User(
accountId = accountInfo.id,
email = accountInfo.email,
profileImage = accountMetaData?.profileImage,
accountInfo = accountInfo,
accountMetaData = accountMetaData,
)
}
}.also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ class GetAccountUseCaseTest : BehaviorSpec() {
result.shouldBeSuccess()
.shouldBeInstanceOf<Account.User>()
.should {
it.accountId shouldBe accountInfo.id
it.email shouldBe accountInfo.email
it.profileImage shouldBe accountMetaData.profileImage
it.accountInfo shouldBe accountInfo
it.accountMetaData shouldBe accountMetaData
}
}

Expand All @@ -94,13 +93,12 @@ class GetAccountUseCaseTest : BehaviorSpec() {
When("GetAccountUseCase를 호출하면") {
val result = useCase().first()

Then("profileImage가 null인 Account.User를 반환한다") {
Then("accountMetaData가 null인 Account.User를 반환한다") {
result.shouldBeSuccess()
.shouldBeInstanceOf<Account.User>()
.should {
it.accountId shouldBe accountInfo.id
it.email shouldBe accountInfo.email
it.profileImage shouldBe null
it.accountInfo shouldBe accountInfo
it.accountMetaData shouldBe null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
@file:OptIn(ExperimentalCoroutinesApi::class)

package io.github.taetae98coding.diary.domain.sync.usecase

import io.github.taetae98coding.diary.core.model.account.Account
import io.github.taetae98coding.diary.core.model.sync.SyncType
import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase
import io.github.taetae98coding.diary.domain.sync.manager.SyncManager
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withTimeoutOrNull
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Provided

Expand All @@ -15,7 +21,12 @@ public class RequestSyncUseCase(
private val syncManager: SyncManager,
) {
public suspend operator fun invoke(type: SyncType) {
when (val account = getAccountUseCase().first().getOrThrow()) {
val account = withTimeoutOrNull(5.seconds) {
getAccountUseCase().mapLatest { it.getOrThrow() }
.first { it.isAuthorized }
} ?: getAccountUseCase().first().getOrThrow()

when (account) {
is Account.Guest -> Unit
is Account.User -> syncManager.requestSync(account.accountId, type)
}
Expand Down
Loading