Skip to content

Commit fa6c716

Browse files
authored
Merge pull request #317 from code-payments/feat/tip-protos-update
feat: associate current user with TwitterUser on app login
2 parents 0241acd + a32b54b commit fa6c716

12 files changed

Lines changed: 161 additions & 48 deletions

File tree

api/src/androidTest/java/com/getcode/models/intents/IntentPrivateTransferTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class IntentPrivateTransferTest {
5959
amount = amount,
6060
isWithdrawal = false,
6161
fee = Kin.fromKin(0),
62-
additionalFees = emptyList()
62+
additionalFees = emptyList(),
63+
tippedUsername = null,
6364
)
6465

6566
assertEquals(rendezvous, intent.id)

api/src/main/java/com/getcode/manager/SessionManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import com.getcode.crypt.MnemonicPhrase
55
import com.getcode.ed25519.Ed25519
66
import com.getcode.model.Domain
7+
import com.getcode.network.TipController
78
import com.getcode.network.client.Client
89
import com.getcode.network.client.awaitEstablishRelationship
910
import com.getcode.network.client.establishRelationshipSingle
@@ -26,6 +27,7 @@ import javax.inject.Singleton
2627
@Singleton
2728
class SessionManager @Inject constructor(
2829
private val client: Client,
30+
private val tipController: TipController
2931
) {
3032
data class SessionState(
3133
val entropyB64: String? = null,
@@ -69,6 +71,9 @@ class SessionManager @Inject constructor(
6971
if (installationId != null) {
7072
client.registerInstallation(organizer.ownerKeyPair, installationId)
7173
}
74+
75+
tipController.checkForConnection()
76+
7277
return client.updatePreferences(organizer)
7378
.onSuccess {
7479
update { it.copy(userPrefsUpdated = true) }

api/src/main/java/com/getcode/model/TwitterUser.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.getcode.model
22

33
import android.webkit.MimeTypeMap
4+
import com.codeinc.gen.user.v1.IdentityService
45
import com.codeinc.gen.user.v1.IdentityService.GetTwitterUserResponse
56
import com.getcode.solana.keys.PublicKey
67

78
data class TwitterUser(
89
val username: String,
10+
val displayName: String,
911
val imageUrl: String,
1012
val followerCount: Int,
1113
val tipAddress: PublicKey,
14+
val verificationStatus: VerificationStatus
1215
) {
1316

1417
val imageUrlSanitized: String
@@ -21,18 +24,23 @@ data class TwitterUser(
2124
return urlWithoutType.plus(".$extension")
2225
}
2326

27+
enum class VerificationStatus {
28+
none, blue, government, unknown
29+
}
30+
2431
companion object {
25-
fun invoke(proto: GetTwitterUserResponse): TwitterUser? {
32+
fun invoke(proto: IdentityService.TwitterUser): TwitterUser? {
2633
val avatarUrl = proto.profilePicUrl
27-
val avatarBytes = proto.profilePicUrlBytes
2834

2935
val tipAddress = runCatching { PublicKey.fromByteString(proto.tipAddress.value) }.getOrNull() ?: return null
3036

3137
return TwitterUser(
32-
username = proto.name,
38+
username = proto.username,
39+
displayName = proto.name,
3340
imageUrl = avatarUrl,
3441
followerCount = proto.followerCount,
35-
tipAddress = tipAddress
42+
tipAddress = tipAddress,
43+
verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown
3644
)
3745
}
3846
}

api/src/main/java/com/getcode/model/intents/IntentPrivateTransfer.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.getcode.model.intents
22

33
import android.content.Context
44
import com.codeinc.gen.transaction.v2.TransactionService
5+
import com.codeinc.gen.transaction.v2.TransactionService.TippedUser.Platform
56
import com.getcode.model.Fee
67
import com.getcode.model.Kin
78
import com.getcode.model.KinAmount
@@ -28,23 +29,33 @@ class IntentPrivateTransfer(
2829
private val fee: Kin,
2930
private val additionalFees: List<Fee>,
3031
private val isWithdrawal: Boolean,
32+
private val tippedUsername: String?,
3133
val resultTray: Tray,
3234

3335
override val actionGroup: ActionGroup,
3436
) : IntentType() {
3537
override fun metadata(): TransactionService.Metadata {
3638
return TransactionService.Metadata.newBuilder()
3739
.setSendPrivatePayment(
38-
TransactionService.SendPrivatePaymentMetadata.newBuilder()
39-
.setDestination(destination.bytes.toSolanaAccount())
40-
.setIsWithdrawal(isWithdrawal)
41-
.setExchangeData(
40+
TransactionService.SendPrivatePaymentMetadata.newBuilder().apply {
41+
setDestination(this@IntentPrivateTransfer.destination.bytes.toSolanaAccount())
42+
setIsWithdrawal(isWithdrawal)
43+
setExchangeData(
4244
TransactionService.ExchangeData.newBuilder()
4345
.setQuarks(grossAmount.kin.quarks)
4446
.setCurrency(grossAmount.rate.currency.name.lowercase())
4547
.setExchangeRate(grossAmount.rate.fx)
4648
.setNativeAmount(grossAmount.fiat)
49+
)
50+
51+
if (tippedUsername != null) {
52+
setIsTip(true)
53+
setTippedUser(TransactionService.TippedUser.newBuilder()
54+
.setPlatformValue(Platform.TWITTER_VALUE)
55+
.setUsername(tippedUsername)
4756
)
57+
}
58+
}
4859
)
4960
.build()
5061
}
@@ -59,6 +70,7 @@ class IntentPrivateTransfer(
5970
fee: Kin,
6071
additionalFees: List<Fee>,
6172
isWithdrawal: Boolean,
73+
tippedUsername: String?
6274
): IntentPrivateTransfer {
6375
if (fee > amount.kin) {
6476
throw IntentPrivateTransferException.InvalidFeeException()
@@ -203,6 +215,7 @@ class IntentPrivateTransfer(
203215
fee = fee,
204216
additionalFees = additionalFees,
205217
isWithdrawal = isWithdrawal,
218+
tippedUsername = tippedUsername,
206219
actionGroup = group,
207220
resultTray = currentTray,
208221
)

api/src/main/java/com/getcode/network/TipController.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.getcode.network
22

3+
import com.getcode.manager.SessionManager
34
import com.getcode.model.CodePayload
45
import com.getcode.model.PrefsString
56
import com.getcode.model.TwitterUser
@@ -18,7 +19,7 @@ typealias TipUser = Pair<String, CodePayload>
1819
@Singleton
1920
class TipController @Inject constructor(
2021
private val client: Client,
21-
prefRepository: PrefRepository,
22+
private val prefRepository: PrefRepository,
2223
) {
2324

2425
val connectedAccount: Flow<String?> = prefRepository.observeOrDefault(PrefsString.KEY_TWITTER_USERNAME, "")
@@ -33,6 +34,19 @@ class TipController @Inject constructor(
3334

3435
private var cachedUsers = mutableMapOf<String, TwitterUser>()
3536

37+
suspend fun checkForConnection() {
38+
val tipAddress = SessionManager.getOrganizer()?.primaryVault ?: return
39+
client.fetchTwitterUser(tipAddress)
40+
.onSuccess {
41+
Timber.d("current user twitter connected @ ${it.username}")
42+
prefRepository.set(PrefsString.KEY_TWITTER_USERNAME, it.username)
43+
}
44+
.onFailure {
45+
it.printStackTrace()
46+
prefRepository.set(PrefsString.KEY_TWITTER_USERNAME, "")
47+
}
48+
}
49+
3650
suspend fun fetch(username: String, payload: CodePayload) {
3751
val metadata = fetch(username)
3852
scannedUserData = username to payload

api/src/main/java/com/getcode/network/client/Client_Identity.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@ suspend fun Client.updatePreferences(organizer: Organizer): Result<Boolean> {
1818
}
1919

2020
suspend fun Client.fetchTwitterUser(username: String): Result<TwitterUser> {
21-
return identityRepository.fetchTwitterUser(username)
21+
return identityRepository.fetchTwitterUserByUsername(username)
22+
}
23+
24+
suspend fun Client.fetchTwitterUser(address: PublicKey): Result<TwitterUser> {
25+
return identityRepository.fetchTwitterUserByAddress(address)
2226
}

api/src/main/java/com/getcode/network/client/Client_Transaction.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ fun Client.transfer(
6161
organizer: Organizer,
6262
rendezvousKey: PublicKey,
6363
destination: PublicKey,
64-
isWithdrawal: Boolean
64+
isWithdrawal: Boolean,
65+
tippedUsername: String? = null,
6566
): Completable {
6667
return transferWithResultSingle(
6768
amount,
@@ -88,12 +89,13 @@ fun Client.transferWithResultSingle(
8889
organizer: Organizer,
8990
rendezvousKey: PublicKey,
9091
destination: PublicKey,
91-
isWithdrawal: Boolean
92+
isWithdrawal: Boolean,
93+
tippedUsername: String? = null,
9294
): Single<Result<Unit>> {
9395
return getTransferPreflightAction(amount.kin)
9496
.andThen(Single.defer {
9597
transactionRepository.transfer(
96-
context, amount, fee, additionalFees, organizer, rendezvousKey, destination, isWithdrawal
98+
context, amount, fee, additionalFees, organizer, rendezvousKey, destination, isWithdrawal, tippedUsername
9799
)
98100
})
99101
.map {
@@ -111,9 +113,10 @@ fun Client.transferWithResult(
111113
organizer: Organizer,
112114
rendezvousKey: PublicKey,
113115
destination: PublicKey,
114-
isWithdrawal: Boolean
116+
isWithdrawal: Boolean,
117+
tippedUsername: String? = null,
115118
): Result<Unit> {
116-
return transferWithResultSingle(amount, fee, additionalFees, organizer, rendezvousKey, destination, isWithdrawal)
119+
return transferWithResultSingle(amount, fee, additionalFees, organizer, rendezvousKey, destination, isWithdrawal, tippedUsername)
117120
.blockingGet()
118121
}
119122

api/src/main/java/com/getcode/network/repository/IdentityRepository.kt

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.codeinc.gen.user.v1.IdentityService.GetTwitterUserRequest
77
import com.codeinc.gen.user.v1.IdentityService.LoginToThirdPartyAppRequest
88
import com.codeinc.gen.user.v1.IdentityService.LoginToThirdPartyAppResponse
99
import com.codeinc.gen.user.v1.IdentityService.UpdatePreferencesRequest
10-
import com.codeinc.gen.user.v1.IdentityService.User
1110
import com.getcode.db.Database
1211
import com.getcode.ed25519.Ed25519
1312
import com.getcode.ed25519.Ed25519.KeyPair
@@ -23,10 +22,8 @@ import com.google.common.collect.Sets
2322
import io.reactivex.rxjava3.core.Flowable
2423
import io.reactivex.rxjava3.core.Single
2524
import kotlinx.coroutines.flow.first
26-
import kotlinx.coroutines.flow.firstOrNull
2725
import kotlinx.coroutines.flow.map
2826
import timber.log.Timber
29-
import java.io.ByteArrayOutputStream
3027
import java.util.Locale
3128
import java.util.concurrent.TimeUnit
3229
import javax.inject.Inject
@@ -296,19 +293,18 @@ class IdentityRepository @Inject constructor(
296293
}
297294
}
298295

299-
suspend fun fetchTwitterUser(username: String): Result<TwitterUser> {
296+
suspend fun fetchTwitterUserByUsername(username: String): Result<TwitterUser> {
300297
val request = GetTwitterUserRequest.newBuilder()
301298
.setUsername(username)
302299
.build()
303300

304301
return try {
305-
Timber.d("fetchTwitterUser")
306302
networkOracle.managedRequest(identityApi.fetchTwitterUser(request))
307303
.map { response ->
308304
when (response.result) {
309305
IdentityService.GetTwitterUserResponse.Result.OK -> {
310306
Timber.d("user ok")
311-
val user = TwitterUser.invoke(response)
307+
val user = TwitterUser.invoke(response.twitterUser)
312308
if (user == null) {
313309
val error =
314310
Throwable("Error: failed to parse twitter user.")
@@ -345,4 +341,53 @@ class IdentityRepository @Inject constructor(
345341
Result.failure(e)
346342
}
347343
}
344+
345+
suspend fun fetchTwitterUserByAddress(address: PublicKey): Result<TwitterUser> {
346+
val request = GetTwitterUserRequest.newBuilder()
347+
.setTipAddress(address.byteArray.toSolanaAccount())
348+
.build()
349+
350+
return try {
351+
networkOracle.managedRequest(identityApi.fetchTwitterUser(request))
352+
.map { response ->
353+
when (response.result) {
354+
IdentityService.GetTwitterUserResponse.Result.OK -> {
355+
Timber.d("user ok")
356+
val user = TwitterUser.invoke(response.twitterUser)
357+
if (user == null) {
358+
val error =
359+
Throwable("Error: failed to parse twitter user.")
360+
ErrorUtils.handleError(error)
361+
Result.failure(error)
362+
} else {
363+
Timber.d("user found")
364+
Result.success(user)
365+
}
366+
}
367+
368+
IdentityService.GetTwitterUserResponse.Result.NOT_FOUND -> {
369+
val error = Throwable("Error: user for address not found.")
370+
ErrorUtils.handleError(error)
371+
Result.failure(error)
372+
}
373+
374+
IdentityService.GetTwitterUserResponse.Result.UNRECOGNIZED -> {
375+
val error = Throwable("Error: fetchTwitterUser Unrecognized request.")
376+
ErrorUtils.handleError(error)
377+
Result.failure(error)
378+
}
379+
380+
else -> {
381+
val error = Throwable("Error: fetchTwitterUser Unknown")
382+
ErrorUtils.handleError(error)
383+
Result.failure(error)
384+
}
385+
}
386+
}.first()
387+
} catch (e: Exception) {
388+
e.printStackTrace()
389+
ErrorUtils.handleError(e)
390+
Result.failure(e)
391+
}
392+
}
348393
}

api/src/main/java/com/getcode/network/repository/TransactionRepository.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ class TransactionRepository @Inject constructor(
122122
organizer: Organizer,
123123
rendezvousKey: PublicKey,
124124
destination: PublicKey,
125-
isWithdrawal: Boolean
125+
isWithdrawal: Boolean,
126+
tippedUsername: String? = null,
126127
): Single<IntentType> {
127128
if (isMock()) return Single.just(
128129
IntentPrivateTransfer(
@@ -135,7 +136,8 @@ class TransactionRepository @Inject constructor(
135136
fee = fee,
136137
additionalFees = emptyList(),
137138
resultTray = organizer.tray,
138-
isWithdrawal = isWithdrawal
139+
isWithdrawal = isWithdrawal,
140+
tippedUsername = tippedUsername
139141
) as IntentType
140142
)
141143
.delay(1, TimeUnit.SECONDS)
@@ -148,7 +150,8 @@ class TransactionRepository @Inject constructor(
148150
amount = amount.copy(kin = amount.kin.toKinTruncating()),
149151
fee = fee,
150152
additionalFees = additionalFees,
151-
isWithdrawal = isWithdrawal
153+
isWithdrawal = isWithdrawal,
154+
tippedUsername = tippedUsername,
152155
)
153156

154157
return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair)

app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.getcode.manager.SessionManager
1414
import com.getcode.model.notifications.NotificationType
1515
import com.getcode.model.notifications.parse
1616
import com.getcode.network.HistoryController
17+
import com.getcode.network.TipController
1718
import com.getcode.network.repository.PushRepository
1819
import com.getcode.ui.components.chat.utils.localizedText
1920
import com.getcode.util.CurrencyUtils
@@ -54,6 +55,9 @@ class CodePushMessagingService : FirebaseMessagingService(),
5455
@Inject
5556
lateinit var historyController: HistoryController
5657

58+
@Inject
59+
lateinit var tipController: TipController
60+
5761
override fun onMessageReceived(remoteMessage: RemoteMessage) {
5862
Timber.d("onMessageReceived")
5963
if (SessionManager.isAuthenticated() == null) {
@@ -82,6 +86,7 @@ class CodePushMessagingService : FirebaseMessagingService(),
8286

8387
if (type == NotificationType.ChatMessage) {
8488
launch { historyController.fetchChats() }
89+
launch { tipController.checkForConnection() }
8590
}
8691
} else {
8792
notify(

0 commit comments

Comments
 (0)