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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protovalidate-kt = "0.1.1"
# protos are independent (flipcash2 does not import ocp), so the packages move on their own
# cadence and there is nothing to keep aligned.
ocp-client-protocol = "0.2.0"
flipcash2-client-protocol = "0.1.0"
flipcash2-client-protocol = "0.2.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 @@ -12,6 +12,7 @@ import com.flipcash.services.models.chat.MediaItem
import com.flipcash.services.repository.ProfileRepository
import com.flipcash.services.user.UserManager
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import com.getcode.utils.TraceType
import com.getcode.utils.trace
import javax.inject.Inject
Expand Down Expand Up @@ -124,6 +125,17 @@ class ProfileController @Inject constructor(
return repository.updateTipCard(owner, hexColor)
}

/**
* Sets the minimum fee another user must pay to initialize a DM chat with
* the caller, replacing any fee already set.
*/
suspend fun setMinDmChatInitFee(fee: Fiat): Result<Unit> {
val owner = userManager.accountCluster?.authority?.keyPair
?: return Result.failure(Throwable("No account cluster in UserManager"))

return repository.setMinDmChatInitFee(owner, fee)
}

/**
* Sets the caller's profile picture to a blob already uploaded via BlobStorage.
* Returns the full set of renditions the server derived from it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.codeinc.flipcash.gen.profile.v1.Model
import com.codeinc.flipcash.gen.profile.v1.emailAddressOrNull
import com.codeinc.flipcash.gen.profile.v1.phoneNumberOrNull
import com.codeinc.flipcash.gen.profile.v1.usernameOrNull
import com.flipcash.services.internal.network.extensions.toFiat
import com.flipcash.services.internal.network.extensions.toId
import com.flipcash.services.internal.network.extensions.toMediaItem
import com.flipcash.services.models.UserProfile
Expand All @@ -30,6 +31,7 @@ class UserProfileMapper @Inject constructor(
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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.codeinc.flipcash.gen.common.v1.Common
import com.codeinc.flipcash.gen.profile.v1.ProfileGrpcKt
import com.codeinc.flipcash.gen.profile.v1.ProfileService
import com.flipcash.services.internal.annotations.FlipcashManagedChannel
import com.flipcash.services.internal.network.extensions.asFiatPaymentAmount
import com.flipcash.services.internal.network.extensions.asUserId
import com.flipcash.services.internal.network.extensions.asUsername
import com.flipcash.services.internal.network.extensions.authenticate
Expand All @@ -14,6 +15,7 @@ import com.flipcash.services.models.SocialAccountUnlinkRequest
import com.flipcash.services.models.chat.BlobId
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.internal.network.core.GrpcApi
import com.getcode.opencode.model.financial.Fiat
import com.getcode.utils.toByteString
import com.codeinc.flipcash.gen.profile.v1.validate
import dev.bmcreations.protovalidate.orThrow
Expand Down Expand Up @@ -158,6 +160,26 @@ internal class ProfileApi @Inject constructor(
}
}

/**
* Sets the minimum fee another user must pay to initialize a DM chat with
* the caller, replacing any fee already set.
*/
suspend fun setMinDmChatInitFee(
owner: Ed25519.KeyPair,
fee: Fiat,
): ProfileService.SetMinDmChatInitFeeResponse {
val request = ProfileService.SetMinDmChatInitFeeRequest.newBuilder()
.setMinDmChatInitFee(fee.asFiatPaymentAmount())
.apply { setAuth(authenticate(owner)) }
.build()

request.validate().orThrow()

return withContext(Dispatchers.IO) {
api.setMinDmChatInitFee(request)
}
}

/**
* removes a social account link from a user
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.flipcash.services.models.chat.TypingState
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.network.jwt.ApiProvider
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import com.getcode.solana.keys.Checksum
import com.getcode.solana.keys.PublicKey
import com.getcode.utils.toByteString
Expand Down Expand Up @@ -93,6 +94,13 @@ internal fun String.asCountryCode(): Common.CountryCode {
return Common.CountryCode.newBuilder().setValue(this).build()
}

internal fun Fiat.asFiatPaymentAmount(): Common.FiatPaymentAmount {
return Common.FiatPaymentAmount.newBuilder()
.setCurrency(currencyCode.name.lowercase())
.setNativeAmount(decimalValue)
.build()
}

internal fun SocialAccountLinkRequest.linkingToken(): ProfileService.LinkSocialAccountRequest.LinkingToken {
val builder = ProfileService.LinkSocialAccountRequest.LinkingToken.newBuilder()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ internal fun Common.UserId.toId(): ID = value.toByteArray().toList()
internal fun Common.Hash.toChecksum(): Checksum = value.toByteArray().toChecksum()
internal fun Common.PublicKey.toPublicKey(): PublicKey = value.toByteArray().toPublicKey()
internal fun Common.PublicKey.toMint(): Mint = value.toByteArray().toMint()
internal fun Common.FiatPaymentAmount.toFiat(): Fiat = Fiat(
fiat = nativeAmount,
currencyCode = CurrencyCode.tryValueOf(currency) ?: CurrencyCode.USD,
)

internal fun PushModels.Payload.asPayload(): NotificationPayload {
val navigationTrigger = navigationOrNull?.typeCase?.let { type ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.flipcash.services.models.GetUserProfileError
import com.flipcash.services.models.LinkSocialAccountError
import com.flipcash.services.models.ProfileIdentifier
import com.flipcash.services.models.SetDisplayNameError
import com.flipcash.services.models.SetMinDmChatInitFeeError
import com.flipcash.services.models.SetProfilePictureError
import com.flipcash.services.models.SetUsernameError
import com.flipcash.services.models.SocialAccountLinkRequest
Expand All @@ -20,6 +21,7 @@ import com.flipcash.services.models.chat.MediaItem
import com.flipcash.services.internal.network.extensions.toMediaItem
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.internal.network.extensions.foldWithSuppression
import com.getcode.opencode.model.financial.Fiat
import javax.inject.Inject

internal class ProfileService @Inject constructor(
Expand Down Expand Up @@ -131,6 +133,26 @@ internal class ProfileService @Inject constructor(
)
}

suspend fun setMinDmChatInitFee(
owner: Ed25519.KeyPair,
fee: Fiat,
): Result<Unit> {
return runCatching {
api.setMinDmChatInitFee(owner, fee)
}.foldWithSuppression(
onSuccess = { response ->
when (response.result) {
ProfileService.SetMinDmChatInitFeeResponse.Result.OK -> Result.success(Unit)
ProfileService.SetMinDmChatInitFeeResponse.Result.DENIED -> Result.failure(SetMinDmChatInitFeeError.Denied())
ProfileService.SetMinDmChatInitFeeResponse.Result.INVALID_AMOUNT -> Result.failure(SetMinDmChatInitFeeError.InvalidAmount())
ProfileService.SetMinDmChatInitFeeResponse.Result.UNRECOGNIZED -> Result.failure(SetMinDmChatInitFeeError.Unrecognized())
null -> Result.failure(SetMinDmChatInitFeeError.Unrecognized())
}
},
onFailure = { Result.failure(it.toValidationOrElse { cause -> SetMinDmChatInitFeeError.Other(cause) }) }
)
}

suspend fun linkSocialAccount(
request: SocialAccountLinkRequest,
owner: Ed25519.KeyPair,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.flipcash.services.models.chat.BlobId
import com.flipcash.services.models.chat.MediaItem
import com.flipcash.services.repository.ProfileRepository
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.model.financial.Fiat
import com.getcode.utils.ErrorUtils

internal class InternalProfileRepository(
Expand Down Expand Up @@ -86,6 +87,14 @@ internal class InternalProfileRepository(
.onFailure { ErrorUtils.handleError(it) }
}

override suspend fun setMinDmChatInitFee(
owner: Ed25519.KeyPair,
fee: Fiat,
): Result<Unit> {
return service.setMinDmChatInitFee(owner, fee)
.onFailure { ErrorUtils.handleError(it) }
}

override suspend fun linkSocialAccount(
request: SocialAccountLinkRequest,
owner: Ed25519.KeyPair
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,16 @@ sealed class UpdateTipCardError(
data class Other(override val cause: Throwable? = null) : UpdateTipCardError(message = cause?.message, cause = cause), NotifiableError
}

sealed class SetMinDmChatInitFeeError(
override val message: String? = null,
override val cause: Throwable? = null
): CodeServerError(message, cause) {
class Denied: SetMinDmChatInitFeeError("Denied")
class InvalidAmount: SetMinDmChatInitFeeError("Invalid amount")
class Unrecognized : SetMinDmChatInitFeeError("Unrecognized"), NotifiableError
data class Other(override val cause: Throwable? = null) : SetMinDmChatInitFeeError(message = cause?.message, cause = cause), NotifiableError
}

// Thrown when a reserved blob failed server-side finalization (moderation / decode / size).
// Terminal: the client must reserve a fresh upload to retry.
class BlobRejectedException(val rejection: BlobRejection) :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipcash.services.models
import android.os.Parcelable
import com.flipcash.services.models.chat.MediaItem
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlin.time.Instant
Expand All @@ -27,6 +28,11 @@ data class UserProfile(
// The user's public Flipcash handle. Public, so it is present for any user —
// null when they haven't claimed one yet.
val username: String? = null,
// The minimum fee another user must pay to initialize a DM chat with this
// user. Public, so it is present for any user, not just the caller. Null
// when the user hasn't set one, in which case the server default applies.
// Update it with ProfileController.setMinDmChatInitFee.
val minDmChatInitFee: Fiat? = null,
): Parcelable {
/** The phone number only when it has been verified — backwards-compatible accessor. */
val verifiedPhoneNumber: String? get() = phoneNumber?.takeIf { it.verified }?.value
Expand All @@ -43,6 +49,7 @@ data class UserProfile(
tipCardColor = null,
userId = null,
username = null,
minDmChatInitFee = null,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import com.flipcash.services.models.UserProfile
import com.flipcash.services.models.chat.BlobId
import com.flipcash.services.models.chat.MediaItem
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.model.financial.Fiat

interface ProfileRepository {
suspend fun getProfile(identifier: ProfileIdentifier, owner: Ed25519.KeyPair): Result<UserProfile>
suspend fun setDisplayName(displayName: String, owner: Ed25519.KeyPair): Result<Unit>
suspend fun setUsername(username: String, owner: Ed25519.KeyPair): Result<Unit>
suspend fun setProfilePicture(blobId: BlobId, owner: Ed25519.KeyPair): Result<MediaItem>
suspend fun updateTipCard(owner: Ed25519.KeyPair, hexColor: String): Result<Unit>
suspend fun setMinDmChatInitFee(owner: Ed25519.KeyPair, fee: Fiat): Result<Unit>
suspend fun linkSocialAccount(request: SocialAccountLinkRequest, owner: Ed25519.KeyPair): Result<SocialAccount>
suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest, owner: Ed25519.KeyPair): Result<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.flipcash.services.user.UserManager
import com.getcode.ed25519.Ed25519
import com.getcode.opencode.model.accounts.AccountCluster
import com.getcode.opencode.model.core.ID
import com.getcode.opencode.model.financial.Fiat
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
Expand Down Expand Up @@ -305,6 +306,7 @@ private class FakeProfileRepository : ProfileRepository {
var setUsernameResult: Result<Unit> = Result.success(Unit)
var setProfilePictureResult: Result<MediaItem> = Result.failure(RuntimeException("not configured"))
var updateTipCardResult: Result<Unit> = Result.success(Unit)
var setMinDmChatInitFeeResult: Result<Unit> = Result.success(Unit)
var linkSocialAccountResult: Result<SocialAccount> = Result.failure(RuntimeException("not configured"))
var unlinkSocialAccountResult: Result<Unit> = Result.success(Unit)

Expand All @@ -313,6 +315,7 @@ private class FakeProfileRepository : ProfileRepository {
override suspend fun setUsername(username: String, owner: Ed25519.KeyPair) = setUsernameResult
override suspend fun setProfilePicture(blobId: BlobId, owner: Ed25519.KeyPair) = setProfilePictureResult
override suspend fun updateTipCard(owner: Ed25519.KeyPair, hexColor: String) = updateTipCardResult
override suspend fun setMinDmChatInitFee(owner: Ed25519.KeyPair, fee: Fiat) = setMinDmChatInitFeeResult
override suspend fun linkSocialAccount(request: SocialAccountLinkRequest, owner: Ed25519.KeyPair) =
linkSocialAccountResult
override suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest, owner: Ed25519.KeyPair) =
Expand Down
Loading