diff --git a/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt b/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt index ea93d45fe1..eecfe166a9 100644 --- a/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt +++ b/apps/flipcash/shared/profile/src/main/kotlin/com/flipcash/shared/profile/ProfileCoordinator.kt @@ -81,6 +81,7 @@ private data class CachedProfile( val socialAccounts: List = emptyList(), val phoneNumber: VerifiableContactMethod? = null, val email: VerifiableContactMethod? = null, + val isUsernameAutoAssigned: Boolean = false, ) { fun toDomain(): UserProfile = UserProfile( displayName = displayName.orEmpty(), @@ -88,6 +89,7 @@ private data class CachedProfile( socialAccounts = socialAccounts.mapNotNull { it.toDomain() }, phoneNumber = phoneNumber, email = email, + isUsernameAutoAssigned = isUsernameAutoAssigned, ) companion object { @@ -97,6 +99,7 @@ private data class CachedProfile( socialAccounts = profile.socialAccounts.map { CachedSocialAccount.fromDomain(it) }, phoneNumber = profile.phoneNumber, email = profile.email, + isUsernameAutoAssigned = profile.isUsernameAutoAssigned, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e7395f7d7..772d70b7a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,7 +69,7 @@ protovalidate-kt = "0.1.3" # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. ocp-client-protocol = "0.6.0" -flipcash2-client-protocol = "0.13.0" +flipcash2-client-protocol = "0.14.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's # `com.googlecode` artifact used to sit alongside it; the two ship separate copies of the metadata, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt index f0991c3695..d0f0ec8cc3 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ProfileController.kt @@ -100,7 +100,12 @@ class ProfileController @Inject constructor( mergeLocalProfile { // Use the server-assigned username when it sent one, rather than assuming the // existing username is unchanged. - it.copy(displayName = displayName, username = assigned ?: it.username) + it.copy( + displayName = displayName, + username = assigned ?: it.username, + // A username the server sent back was derived from the display name. + isUsernameAutoAssigned = if (assigned != null) true else it.isUsernameAutoAssigned, + ) } } } @@ -118,7 +123,10 @@ class ProfileController @Inject constructor( return repository.setUsername(username, owner) // Reflect the change locally so anything observing the profile (e.g. a setup flow // deciding which steps remain) sees it without waiting for a refresh. - .onSuccess { mergeLocalProfile { it.copy(username = username) } } + .onSuccess { + // Choosing a username clears the auto-assigned flag. + mergeLocalProfile { it.copy(username = username, isUsernameAutoAssigned = false) } + } } /** diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt index 415dcdad99..523acba6e6 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserFlagsMapper.kt @@ -32,7 +32,7 @@ internal class UserFlagsMapper @Inject constructor(): enablePhoneNumberSend = from.enablePhoneNumberSend, minimumHolderValue = Fiat(quarks = from.minimumHolderValue), requireCoinbaseEmailVerification = from.requireCoinbaseEmailVerification, - tipPresets = from.tipPresetsList.map { it.toDomain() }, + tipPresets = from.sendPresetsList.map { it.toDomain() }, usernameMinBalance = Fiat(quarks = from.usernameMinBalance), messageEditWindow = if (from.hasMessageEditWindow()) { from.messageEditWindow.seconds.toDuration(DurationUnit.SECONDS) @@ -44,7 +44,7 @@ internal class UserFlagsMapper @Inject constructor(): } } -private fun FlipcashAccountService.TipPresets.toDomain(): TipPresets = TipPresets( +private fun FlipcashAccountService.SendPresets.toDomain(): TipPresets = TipPresets( region = region.value, minimum = minimum, low = low, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt index 68c2196f3c..a0cce638da 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/UserProfileMapper.kt @@ -27,11 +27,12 @@ class UserProfileMapper @Inject constructor( joinedAt = if (from.hasJoinTs()) { Instant.fromEpochSeconds(from.joinTs.seconds, from.joinTs.nanos) } else null, - tipCardColor = if (from.hasTipCardCustomization()) from.tipCardCustomization.color.hex else null, + tipCardColor = if (from.hasFlipcardCustomization()) from.flipcardCustomization.color.hex else null, userId = if (from.hasUserId()) from.userId.toId() else null, // Public, so it is returned for any user — absent only when unclaimed. username = from.usernameOrNull?.value, minDmChatInitFee = if (from.hasMinDmChatInitFee()) from.minDmChatInitFee.toFiat() else null, + isUsernameAutoAssigned = from.isUsernameAutoAssigned, ) } } \ No newline at end of file diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt index f546aadfc1..bfee17a61f 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ProfileApi.kt @@ -140,11 +140,11 @@ internal class ProfileApi @Inject constructor( /** * Updates the caller's tip card customization with the given hex color. */ - suspend fun updateTipCard( + suspend fun updateFlipcard( owner: Ed25519.KeyPair, hexColor: String, - ): ProfileService.UpdateTipCardResponse { - val request = ProfileService.UpdateTipCardRequest.newBuilder() + ): ProfileService.UpdateFlipcardResponse { + val request = ProfileService.UpdateFlipcardRequest.newBuilder() .setColor( Common.Color.newBuilder() .setHex(hexColor) @@ -156,7 +156,7 @@ internal class ProfileApi @Inject constructor( request.validate().orThrow() return withContext(Dispatchers.IO) { - api.updateTipCard(request) + api.updateFlipcard(request) } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt index 7dd5d158f1..e3d35fa603 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ProfileService.kt @@ -120,14 +120,14 @@ internal class ProfileService @Inject constructor( hexColor: String, ): Result { return runCatching { - api.updateTipCard(owner, hexColor) + api.updateFlipcard(owner, hexColor) }.foldWithSuppression( onSuccess = { response -> when (response.result) { - ProfileService.UpdateTipCardResponse.Result.OK -> Result.success(Unit) - ProfileService.UpdateTipCardResponse.Result.DENIED -> Result.failure(UpdateTipCardError.Denied()) - ProfileService.UpdateTipCardResponse.Result.INVALID_COLOR -> Result.failure(UpdateTipCardError.InvalidColor()) - ProfileService.UpdateTipCardResponse.Result.UNRECOGNIZED -> Result.failure(UpdateTipCardError.Unrecognized()) + ProfileService.UpdateFlipcardResponse.Result.OK -> Result.success(Unit) + ProfileService.UpdateFlipcardResponse.Result.DENIED -> Result.failure(UpdateTipCardError.Denied()) + ProfileService.UpdateFlipcardResponse.Result.INVALID_COLOR -> Result.failure(UpdateTipCardError.InvalidColor()) + ProfileService.UpdateFlipcardResponse.Result.UNRECOGNIZED -> Result.failure(UpdateTipCardError.Unrecognized()) null -> Result.failure(UpdateTipCardError.Unrecognized()) } }, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt index 1d5f8cc827..2b66d203d9 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/UserProfile.kt @@ -33,6 +33,11 @@ data class UserProfile( // when the user hasn't set one, in which case the server default applies. // Update it with ProfileController.setMinDmChatInitFee. val minDmChatInitFee: Fiat? = null, + // True when the current username was assigned by the server from the display + // name rather than chosen with setUsername. Private: only set on the caller's + // own profile; false for anyone else's and when there is no username. Choosing + // a different username clears it. + val isUsernameAutoAssigned: Boolean = false, ): Parcelable { /** The phone number only when it has been verified — backwards-compatible accessor. */ val verifiedPhoneNumber: String? get() = phoneNumber?.takeIf { it.verified }?.value @@ -50,6 +55,7 @@ data class UserProfile( userId = null, username = null, minDmChatInitFee = null, + isUsernameAutoAssigned = false, ) } } diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt index c0c547c3ab..2376744dff 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ProfileControllerTest.kt @@ -238,6 +238,27 @@ class ProfileControllerTest { verify { userManager.set(match { it.displayName == "Test" && it.username == "old" }) } } + @Test + fun `setDisplayName flags the username as auto-assigned when the server assigns one`() = runTest { + stubOwner() + every { userManager.profile } returns stubProfile() + repository.setDisplayNameResult = Result.success("assigned") + + controller.setDisplayName("Test") + + verify { userManager.set(match { it.isUsernameAutoAssigned }) } + } + + @Test + fun `setUsername clears the auto-assigned flag`() = runTest { + stubOwner() + every { userManager.profile } returns stubProfile().copy(username = "assigned", isUsernameAutoAssigned = true) + + controller.setUsername("chosen") + + verify { userManager.set(match { it.username == "chosen" && !it.isUsernameAutoAssigned }) } + } + // endregion // region linkTwitterXAccount diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt index 76babb3270..4f3834aeca 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserFlagsMapperTest.kt @@ -161,8 +161,8 @@ class UserFlagsMapperTest { @Test fun `maps tip presets`() { val proto = FlipcashAccountService.UserFlags.newBuilder() - .addTipPresets( - FlipcashAccountService.TipPresets.newBuilder() + .addSendPresets( + FlipcashAccountService.SendPresets.newBuilder() .setRegion(com.codeinc.flipcash.gen.common.v1.Common.Region.newBuilder().setValue("usd")) .setMinimum(1.0) .setLow(2.0) diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt index 3c41cd8e6a..b36347eb2f 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/internal/domain/UserProfileMapperTest.kt @@ -110,4 +110,10 @@ class UserProfileMapperTest { val proto = userProfile { displayName = "Heidi" } assertNull(mapper.map(proto).profilePicture) } + + @Test + fun `maps is_username_auto_assigned`() { + assertEquals(true, mapper.map(userProfile { isUsernameAutoAssigned = true }).isUsernameAutoAssigned) + assertEquals(false, mapper.map(userProfile { displayName = "Ivan" }).isUsernameAutoAssigned) + } }