Skip to content

Commit 6f9e99c

Browse files
author
Jeff Yanta
committed
Merge branch 'develop'
2 parents 00bb911 + c9b488c commit 6f9e99c

40 files changed

Lines changed: 621 additions & 305 deletions

api/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ dependencies {
6363
implementation(Libs.okhttp)
6464
implementation(Libs.mixpanel)
6565

66+
implementation(platform(Libs.firebase_bom))
67+
implementation(Libs.firebase_appcheck)
68+
implementation(Libs.firebase_appcheck_debug)
69+
implementation(Libs.firebase_appcheck_playintegrity)
70+
6671
implementation(Libs.androidx_paging_runtime)
6772

6873
kapt(Libs.androidx_room_compiler)

api/src/main/java/com/getcode/crypt/MnemonicPhrase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class MnemonicPhrase(val kind: Kind, val words: List<String>) {
1616

1717
fun getSolanaKeyPair(context: Context, path: DerivePath = DerivePath.primary): Ed25519.KeyPair {
1818
val mnemonicCode = MnemonicCode(context.resources)
19-
val mnemonicSeed = MnemonicCode.toSeed(words, "")
19+
val mnemonicSeed = MnemonicCode.toSeed(words, path.password.orEmpty())
2020
mnemonicCode.check(words)
2121

2222
return Derive.path(mnemonicSeed, path)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.getcode.ed25519.Ed25519
55
import com.getcode.solana.keys.Signature
66
import com.getcode.model.intents.actions.ActionType
77
import com.getcode.model.intents.actions.numberActions
8+
import com.getcode.network.appcheck.toDeviceToken
89
import com.getcode.network.repository.*
910
import com.getcode.solana.keys.PublicKey
1011

@@ -42,12 +43,17 @@ abstract class IntentType {
4243
.build()
4344
}
4445

45-
fun requestToSubmitActions(owner: Ed25519.KeyPair): TransactionService.SubmitIntentRequest {
46+
fun requestToSubmitActions(owner: Ed25519.KeyPair, deviceToken: String? = null): TransactionService.SubmitIntentRequest {
4647
val submitActionsBuilder = TransactionService.SubmitIntentRequest.SubmitActions.newBuilder()
4748
submitActionsBuilder.owner = owner.publicKeyBytes.toSolanaAccount()
4849
submitActionsBuilder.id = id.toIntentId()
4950
submitActionsBuilder.metadata = metadata()
5051
submitActionsBuilder.addAllActions(actionGroup.actions.map { it.action() })
52+
53+
if (deviceToken != null) {
54+
submitActionsBuilder.setDeviceToken(deviceToken.toDeviceToken())
55+
}
56+
5157
submitActionsBuilder.signature = submitActionsBuilder.sign(owner)
5258

5359
return TransactionService.SubmitIntentRequest.newBuilder()

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ class HistoryController @Inject constructor(
7676
private fun owner(): KeyPair? = SessionManager.getKeyPair()
7777

7878
suspend fun fetchChats() {
79+
pagerMap.clear()
80+
chatFlows.clear()
81+
7982
val containers = fetchChatsWithoutMessages()
8083
Timber.d("chats fetched = ${containers.count()}")
8184
_chats.value = containers
@@ -93,10 +96,6 @@ class HistoryController @Inject constructor(
9396
}
9497

9598
_chats.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
96-
97-
pagerMap.entries.onEach { (id, pagingSource) ->
98-
pagingSource.invalidate()
99-
}
10099
}
101100

102101
suspend fun advanceReadPointer(chatId: ID) {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.getcode.network.appcheck
2+
3+
import com.codeinc.gen.common.v1.Model
4+
import com.getcode.api.BuildConfig
5+
import com.google.firebase.Firebase
6+
import com.google.firebase.appcheck.AppCheckToken
7+
import com.google.firebase.appcheck.AppCheckTokenResult
8+
import com.google.firebase.appcheck.appCheck
9+
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
10+
import com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider
11+
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
12+
import io.reactivex.rxjava3.core.BackpressureStrategy
13+
import io.reactivex.rxjava3.core.Flowable
14+
import io.reactivex.rxjava3.core.Single
15+
import kotlinx.coroutines.flow.Flow
16+
import kotlinx.coroutines.reactive.asFlow
17+
import timber.log.Timber
18+
import java.lang.Exception
19+
20+
data class DeviceTokenResult(
21+
val token: AppCheckToken?
22+
)
23+
24+
object AppCheck {
25+
26+
fun register() {
27+
if (BuildConfig.DEBUG) {
28+
Firebase.appCheck.installAppCheckProviderFactory(
29+
DebugAppCheckProviderFactory.getInstance()
30+
)
31+
} else {
32+
Firebase.appCheck.installAppCheckProviderFactory(
33+
PlayIntegrityAppCheckProviderFactory.getInstance()
34+
)
35+
}
36+
}
37+
38+
private fun handleAppCheckError(error: Exception): Boolean {
39+
val match = "code: \\d+".toRegex().find(error.message.orEmpty())
40+
val errorCode = match?.value?.removePrefix("code: ")?.toInt() ?: -1
41+
Timber.e("Failed to get appcheck token: errorCode=$errorCode ${error.message}")
42+
43+
return errorCode == 403 // bad attestation
44+
|| errorCode >= 500
45+
}
46+
47+
@Deprecated("Replace with Flow variant")
48+
fun limitedUseTokenSingle(): Single<DeviceTokenResult> {
49+
return Single.create { emitter ->
50+
Firebase.appCheck.limitedUseAppCheckToken
51+
.addOnSuccessListener {
52+
Timber.d("attestation passed")
53+
emitter.onSuccess(DeviceTokenResult(it))
54+
}
55+
.addOnFailureListener { error ->
56+
if (!handleAppCheckError(error)) {
57+
emitter.onError(error)
58+
return@addOnFailureListener
59+
}
60+
61+
emitter.onSuccess(DeviceTokenResult(null))
62+
}
63+
}
64+
}
65+
66+
@Deprecated("Replace with Flow variant")
67+
fun limitedUseTokenFlowable(
68+
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER
69+
): Flowable<DeviceTokenResult> {
70+
return Flowable.create({ emitter ->
71+
Firebase.appCheck.limitedUseAppCheckToken
72+
.addOnSuccessListener {
73+
Timber.d("attestation passed")
74+
emitter.onNext(DeviceTokenResult(it))
75+
}
76+
.addOnFailureListener { error ->
77+
if (!handleAppCheckError(error)) {
78+
emitter.onError(error)
79+
return@addOnFailureListener
80+
}
81+
82+
emitter.onNext(DeviceTokenResult(null))
83+
}
84+
}, backpressureStrategy)
85+
}
86+
87+
fun limitedUseToken(
88+
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER
89+
): Flow<DeviceTokenResult> {
90+
return limitedUseTokenFlowable(backpressureStrategy).asFlow()
91+
}
92+
}
93+
94+
fun AppCheckToken.toDeviceToken() = Model.DeviceToken.newBuilder().setValue(this.token).build()
95+
fun String.toDeviceToken() = Model.DeviceToken.newBuilder().setValue(this).build()

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ suspend fun Client.receiveRemoteSuspend(giftCard: GiftCardAccount): KinAmount =
185185
)
186186

187187
balanceController.fetchBalanceSuspend()
188-
// TODO: fetch chats here somehow?
189188

190189
return@withContext kinAmount
191190
}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ import javax.inject.Inject
2525
import kotlin.coroutines.resume
2626
import kotlin.coroutines.resumeWithException
2727

28-
data class Request(
29-
val amount: KinAmount,
30-
val payload: CodePayload,
31-
)
3228

3329
class PaymentRepository @Inject constructor(
3430
@ApplicationContext private val context: Context,

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,33 @@ package com.getcode.network.repository
22

33
import com.codeinc.gen.phone.v1.PhoneVerificationService
44
import com.getcode.db.Database
5-
import com.getcode.db.InMemoryDao
65
import com.getcode.ed25519.Ed25519
7-
import com.getcode.model.PrefsBool
8-
import com.getcode.model.PrefsString
9-
import com.getcode.network.core.NetworkOracle
106
import com.getcode.network.api.PhoneApi
7+
import com.getcode.network.appcheck.AppCheck
8+
import com.getcode.network.appcheck.toDeviceToken
9+
import com.getcode.network.core.NetworkOracle
10+
import com.google.firebase.Firebase
11+
import com.google.firebase.appcheck.AppCheckToken
12+
import com.google.firebase.appcheck.appCheck
13+
import io.reactivex.rxjava3.core.BackpressureStrategy
1114
import io.reactivex.rxjava3.core.Flowable
1215
import io.reactivex.rxjava3.core.Single
1316
import kotlinx.coroutines.flow.MutableStateFlow
1417
import java.io.ByteArrayOutputStream
1518
import javax.inject.Inject
1619
import javax.inject.Singleton
1720

21+
22+
fun appCheckToken(
23+
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER
24+
): Flowable<AppCheckToken> {
25+
return Flowable.create({ emitter ->
26+
Firebase.appCheck.limitedUseAppCheckToken
27+
.addOnSuccessListener { emitter.onNext(it) }
28+
.addOnFailureListener { emitter.onError(it) }
29+
}, backpressureStrategy)
30+
}
31+
1832
@Singleton
1933
class PhoneRepository @Inject constructor(
2034
private val phoneApi: PhoneApi,
@@ -37,14 +51,22 @@ class PhoneRepository @Inject constructor(
3751
if (isMock()) return Single.just(PhoneVerificationService.SendVerificationCodeResponse.Result.OK)
3852
.toFlowable()
3953

40-
val request =
41-
PhoneVerificationService.SendVerificationCodeRequest.newBuilder()
42-
.setPhoneNumber(phoneValue.toPhoneNumber())
43-
.build()
54+
return AppCheck.limitedUseTokenFlowable()
55+
.flatMap { tokenResult ->
56+
val request =
57+
PhoneVerificationService.SendVerificationCodeRequest.newBuilder()
58+
.setPhoneNumber(phoneValue.toPhoneNumber())
59+
.apply {
60+
if (tokenResult.token != null) {
61+
setDeviceToken(tokenResult.token.toDeviceToken())
62+
}
63+
}.build()
4464

45-
return phoneApi.sendVerificationCode(request)
46-
.map { it.result }
47-
.let { networkOracle.managedRequest(it) }
65+
66+
phoneApi.sendVerificationCode(request)
67+
.map { it.result }
68+
.let { networkOracle.managedRequest(it) }
69+
}
4870
}
4971

5072
fun checkVerificationCode(
@@ -68,7 +90,7 @@ class PhoneRepository @Inject constructor(
6890
): Flowable<GetAssociatedPhoneNumberResponse> {
6991
if (isMock()) {
7092
return Flowable.just(
71-
GetAssociatedPhoneNumberResponse(true, true, false,"+12223334455")
93+
GetAssociatedPhoneNumberResponse(true, true, false, "+12223334455")
7294
)
7395
}
7496

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.getcode.model.intents.IntentType
2424
import com.getcode.model.intents.IntentUpgradePrivacy
2525
import com.getcode.model.intents.ServerParameter
2626
import com.getcode.network.api.TransactionApiV2
27+
import com.getcode.network.appcheck.AppCheck
2728
import com.getcode.solana.keys.AssociatedTokenAccount
2829
import com.getcode.solana.keys.PublicKey
2930
import com.getcode.solana.organizer.AccountType
@@ -86,7 +87,11 @@ class TransactionRepository @Inject constructor(
8687
.delay(1, TimeUnit.SECONDS)
8788

8889
val createAccounts = IntentCreateAccounts.newInstance(organizer)
89-
return submit(createAccounts, organizer.tray.owner.getCluster().authority.keyPair)
90+
91+
return AppCheck.limitedUseTokenSingle()
92+
.flatMap { tokenResult ->
93+
submit(createAccounts, organizer.tray.owner.getCluster().authority.keyPair, tokenResult.token?.token)
94+
}
9095
}
9196

9297
fun transfer(
@@ -236,7 +241,7 @@ class TransactionRepository @Inject constructor(
236241
return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair)
237242
}
238243

239-
private fun submit(intent: IntentType, owner: KeyPair): Single<IntentType> {
244+
private fun submit(intent: IntentType, owner: KeyPair, deviceToken: String? = null): Single<IntentType> {
240245
Timber.i("Submit ${intent.javaClass.simpleName}")
241246
val subject = SingleSubject.create<IntentType>()
242247

@@ -319,7 +324,7 @@ class TransactionRepository @Inject constructor(
319324

320325
// 1. Send `submitActions` request with
321326
// actions generated by the intent
322-
serverMessageStream.onNext(intent.requestToSubmitActions(owner))
327+
serverMessageStream.onNext(intent.requestToSubmitActions(owner, deviceToken))
323328

324329
return subject
325330
}

app/src/main/java/com/getcode/App.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import android.app.Application
44
import androidx.appcompat.app.AppCompatDelegate
55
import com.bugsnag.android.Bugsnag
66
import com.getcode.manager.AuthManager
7+
import com.getcode.network.appcheck.AppCheck
78
import com.getcode.utils.ErrorUtils
89
import com.getcode.view.main.bill.CashBillAssets
10+
import com.google.firebase.Firebase
11+
import com.google.firebase.initialize
912
import dagger.hilt.android.HiltAndroidApp
1013
import io.reactivex.rxjava3.plugins.RxJavaPlugins
1114
import timber.log.Timber
@@ -23,6 +26,9 @@ class App : Application() {
2326

2427
CashBillAssets.load(this)
2528

29+
Firebase.initialize(this)
30+
AppCheck.register()
31+
2632
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
2733

2834
RxJavaPlugins.setErrorHandler {

0 commit comments

Comments
 (0)