Skip to content

feat: Data transfer support - #825

Open
LouisCAD wants to merge 26 commits into
mainfrom
data-transfer-support
Open

feat: Data transfer support#825
LouisCAD wants to merge 26 commits into
mainfrom
data-transfer-support

Conversation

@LouisCAD

Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds account data-transfer recovery by detecting device changes and re-deriving restored authentication tokens.

Changes:

  • Persists Android ID bindings for user tokens via Room.
  • Adds restoration orchestration, request blocking, retry/give-up UI, and error handling.
  • Moves token derivation into Auth and updates account APIs.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
CrossAppLogin/README.md Updates account-removal guidance.
CrossAppLogin/Back/.../SharedDeviceIdStorage.kt Reuses the shared Android ID helper.
CrossAppLogin/Back/.../BaseCrossAppLoginViewModel.kt Uses Auth token derivation APIs.
Common/.../AndroidId.kt Adds the shared Android ID helper.
Auth/.../UserAccountUtils.kt Stores token-device bindings.
Auth/.../TokenInterceptor.kt Waits for restoration handling.
Auth/.../room/UserDatabase.kt Adds the binding entity and migration.
Auth/.../room/UserDao.kt Adds binding queries and user-list APIs.
Auth/.../PersistedCurrentUserAccountUtils.kt Accepts deferred cleanup providers.
Auth/.../models/TokenDeviceBinding.kt Defines token-device persistence.
Auth/.../DerivedTokenGeneratorImpl.kt Moves derivation implementation into Auth.
Auth/.../DerivedTokenGenerator.kt Exposes the derivation contract.
Auth/.../DerivedTokenGenerator.IssueExtensions.kt Centralizes issue policies.
Auth/.../CredentialManager.kt Integrates modern account utilities.
Auth/.../backup/RestoringFromBackupFailedScreen.kt Adds restoration failure UI.
Auth/.../backup/RestoreFromBackupManagerImpl.kt Implements restoration handling.
Auth/.../backup/RestoreFromBackupManager.kt Defines restoration state and API.
Auth/.../AbstractCurrentUserAccountUtils.kt Accepts deferred cleanup providers.
Auth/schemas/.../9.json Records Room schema version 9.
Auth/build.gradle.kts Adds Compose UI dependencies.

is AppIntegrityIssue.RetryLater, is AppIntegrityIssue.Internal -> true
is AppIntegrityIssue.DeviceIssue, is AppIntegrityIssue.DevError, is AppIntegrityIssue.SuspiciousError -> false
}
is Issue.ErrorResponse -> true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed.

Comment on lines +68 to +71
override val state: SharedFlow<State> = flow {
performRestorationHandlingIfNeeded()
emit(State.Settled)
}.distinctUntilChanged().shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can you make a PR targeting this branch that tests it? I'm curious to see the approach you will pick.

Comment thread Auth/src/main/kotlin/com/infomaniak/core/auth/UserAccountUtils.kt
Comment thread Auth/src/main/kotlin/com/infomaniak/core/auth/CredentialManager.kt
Comment thread Auth/src/main/kotlin/com/infomaniak/core/auth/room/UserDao.kt
Comment thread Auth/src/main/kotlin/com/infomaniak/core/auth/UserAccountUtils.kt Outdated
@LouisCAD

Copy link
Copy Markdown
Contributor Author

TODO: Ensure we can disable this for the Authenticator app.

Copilot AI commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

@LouisCAD I've opened a new pull request, #826, to work on those changes. Once the pull request is ready, I'll request review from you.

private suspend fun FlowCollector<State>.performRestorationHandlingIfNeeded() {
val users = userDao.allUsers()
val currentAndroidId = getAndroidId()
restoreAccounts(currentAndroidId = currentAndroidId, allUsers = users)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accounts or users ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it easier to reason about like this, but let's talk about it offline.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rotateTokenForUsers, allusers is too contextual.

currentAndroidId: String,
allUsers: List<User>,
) {
val usersToDeriveTokensFor: List<User> = coroutineScope {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An early return if allUsers is empty ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not going to change much, but added it.

if (result !is Xor.Second) return@also
val issue = result.value
val errorMessage = "Failed to derive token"
val sentryUser = io.sentry.protocol.User().also { it.id = user.id.toString() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fully qualified import. Could Be an alias ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be, but for one usage, and for a such a short FQDN, I thought this was good enough.

}
}

private const val TAG = "RestoreFromBackupManagerImpl"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not put this in a companion object ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no need for a companion object which would lightly bloat the binary.

fun RestoringFromBackupFailedScreen(
state: RestoreFromBackupManager.State.RestoringFromBackupFailed,
modifier: Modifier = Modifier,
) = AspectRatioFlow(modifier) { isLandscape ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term Flow here is weird.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same as in FlowRow and FlowColumn, only that it can be any of Column or Row, here.

No Composable functions return a Flow, so at least, we don't have any ambiguity here.

state: RestoreFromBackupManager.State.RestoringFromBackupFailed,
modifier: Modifier = Modifier,
) = AspectRatioFlow(modifier) { isLandscape ->
if (isLandscape) Row(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having no bracket here but having them in the else make this code hard to read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

retry = {},
giveUp = {}
))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want to have UI things in the Auth module.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss this offline.

}

internal fun Issue.shouldRetryAutomatically(): Boolean = when (this) {
is Issue.AppIntegrityCheckFailed -> when (details.issue) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of the when in a when.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split.

private val userDataCleanableList: List<AssociatedUserDataCleanable> = emptyList(),
private val userDataCleanableList: () -> List<AssociatedUserDataCleanable> = { emptyList() },
override val userDatabase: UserDatabase = UserDatabase.instantiateDataBase(appContext),
restoreFromBackupManager: RestoreFromBackupManager = RestoreFromBackupManager.instance,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case where restoreFromBackupManager is not RestoreFromBackupManager.instance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, testing.

@LouisCAD
LouisCAD force-pushed the data-transfer-support branch 2 times, most recently from 87eb958 to bee0f91 Compare August 11, 2026 10:47
@LouisCAD
LouisCAD requested a review from tevincent August 12, 2026 07:32
LouisCAD and others added 18 commits August 12, 2026 13:13
This commit also renames LiveData returning getAll() to allAsLiveData()
This could cause notification and device/app registration to
not be done again after the user got logged out and re-logged in.
This avoids clashes with the splashscreen's logo
We are not going to allow reusing them.
However, we will probably consider using the "standard" API (over the "classic" one),
to allow generating many tokens for cheaper thanks to the shared costly operation.
@LouisCAD
LouisCAD force-pushed the data-transfer-support branch from 7fa8718 to 66a859f Compare August 12, 2026 11:13
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants