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
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ private data class CachedProfile(
val socialAccounts: List<CachedSocialAccount> = emptyList(),
val phoneNumber: VerifiableContactMethod? = null,
val email: VerifiableContactMethod? = null,
val isUsernameAutoAssigned: Boolean = false,
) {
fun toDomain(): UserProfile = UserProfile(
displayName = displayName.orEmpty(),
username = username,
socialAccounts = socialAccounts.mapNotNull { it.toDomain() },
phoneNumber = phoneNumber,
email = email,
isUsernameAutoAssigned = isUsernameAutoAssigned,
)

companion object {
Expand All @@ -97,6 +99,7 @@ private data class CachedProfile(
socialAccounts = profile.socialAccounts.map { CachedSocialAccount.fromDomain(it) },
phoneNumber = profile.phoneNumber,
email = profile.email,
isUsernameAutoAssigned = profile.isUsernameAutoAssigned,
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
}
Expand All @@ -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) }
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -156,7 +156,7 @@ internal class ProfileApi @Inject constructor(
request.validate().orThrow()

return withContext(Dispatchers.IO) {
api.updateTipCard(request)
api.updateFlipcard(request)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ internal class ProfileService @Inject constructor(
hexColor: String,
): Result<Unit> {
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())
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -50,6 +55,7 @@ data class UserProfile(
userId = null,
username = null,
minDmChatInitFee = null,
isUsernameAutoAssigned = false,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,27 @@ class ProfileControllerTest {
verify { userManager.set(match<UserProfile> { 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<UserProfile> { 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<UserProfile> { it.username == "chosen" && !it.isUsernameAutoAssigned }) }
}

// endregion

// region linkTwitterXAccount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading