diff --git a/.github/workflows/build-upload-android-alpha.yml b/.github/workflows/build-upload-android-alpha.yml index 078585de7..3c239aee6 100644 --- a/.github/workflows/build-upload-android-alpha.yml +++ b/.github/workflows/build-upload-android-alpha.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@master + with: + fetch-depth: 0 - name: Setup Java env uses: actions/setup-java@v3 diff --git a/.github/workflows/build-upload-android-internal.yml b/.github/workflows/build-upload-android-internal.yml index f9232b94b..abbbeed5b 100644 --- a/.github/workflows/build-upload-android-internal.yml +++ b/.github/workflows/build-upload-android-internal.yml @@ -17,6 +17,8 @@ jobs: steps: - uses: actions/checkout@master + with: + fetch-depth: 0 - name: Setup Java env uses: actions/setup-java@v3 diff --git a/api/build.gradle.kts b/api/build.gradle.kts index dac4b0039..3fe8eb31c 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,3 +1,5 @@ +import com.google.protobuf.gradle.protobuf + plugins { id(Plugins.android_library) id(Plugins.kotlin_android) @@ -70,10 +72,14 @@ android { dependencies { implementation(project(":common:resources")) + api(project(":service:models")) + implementation(project(":crypto:ed25519")) + implementation(project(":crypto:kin")) implementation(Libs.rxjava) implementation(Libs.kotlinx_coroutines_core) implementation(Libs.kotlinx_serialization_json) + implementation(Libs.kotlinx_datetime) implementation(Libs.inject) implementation(Libs.grpc_okhttp) diff --git a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt index 833c1bf6c..8033b9c18 100644 --- a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt +++ b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt @@ -269,6 +269,10 @@ class AnalyticsManager @Inject constructor( ) } + override fun tipCardLinked() { + track(Name.TipCardLinked) + } + override fun backgroundSwapInitiated() { track(Name.BackgroundSwap) } @@ -319,11 +323,12 @@ class AnalyticsManager @Inject constructor( Login("Login"), CreateAccount("Create Account"), UnintentionalLogout("Unintentional Logout"), + TipCardLinked("Tip Card Linked"), //Bill Bill("Bill"), Request("Request Card"), - TipCard("TIp Card"), + TipCard("Tip Card"), //Transfer Transfer("Transfer"), diff --git a/api/src/main/java/com/getcode/analytics/AnalyticsService.kt b/api/src/main/java/com/getcode/analytics/AnalyticsService.kt index 2f00587c8..fea464b54 100644 --- a/api/src/main/java/com/getcode/analytics/AnalyticsService.kt +++ b/api/src/main/java/com/getcode/analytics/AnalyticsService.kt @@ -45,6 +45,7 @@ interface AnalyticsService { fun withdrawal(amount: KinAmount, successful: Boolean) fun tipCardShown(username: String) + fun tipCardLinked() fun backgroundSwapInitiated() fun unintentionalLogout() @@ -93,6 +94,7 @@ class AnalyticsServiceNull : AnalyticsService { override fun upgradePrivacy(successful: Boolean, intentId: PublicKey, actionCount: Int) = Unit override fun onBillReceived() = Unit override fun tipCardShown(username: String) = Unit + override fun tipCardLinked() = Unit override fun backgroundSwapInitiated() = Unit override fun unintentionalLogout() = Unit override fun appSettingToggled(setting: AppSetting, value: Boolean) = Unit diff --git a/api/src/main/java/com/getcode/db/ConversationDao.kt b/api/src/main/java/com/getcode/db/ConversationDao.kt index dac6e0c89..ff98c92ad 100644 --- a/api/src/main/java/com/getcode/db/ConversationDao.kt +++ b/api/src/main/java/com/getcode/db/ConversationDao.kt @@ -1,5 +1,7 @@ package com.getcode.db +import androidx.paging.PagingData +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert @@ -19,6 +21,10 @@ interface ConversationDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertConversations(vararg conversation: Conversation) + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM conversations") + fun observeConversations(): PagingSource + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM conversations LEFT JOIN conversation_pointers ON conversations.idBase58 = conversation_pointers.conversationIdBase58 WHERE conversations.idBase58 = :id") fun observeConversation(id: String): Flow diff --git a/api/src/main/java/com/getcode/mapper/ConversationMapper.kt b/api/src/main/java/com/getcode/mapper/ConversationMapper.kt index d814f13f8..548c58dc9 100644 --- a/api/src/main/java/com/getcode/mapper/ConversationMapper.kt +++ b/api/src/main/java/com/getcode/mapper/ConversationMapper.kt @@ -4,7 +4,6 @@ import com.getcode.model.Conversation import com.getcode.model.chat.Chat import com.getcode.model.chat.ChatType import com.getcode.model.chat.self -import com.getcode.network.TipController import com.getcode.network.localized import com.getcode.network.repository.base58 import com.getcode.util.resources.ResourceHelper diff --git a/api/src/main/java/com/getcode/model/Feature.kt b/api/src/main/java/com/getcode/model/Feature.kt index ebcc72bf0..f1102cfd0 100644 --- a/api/src/main/java/com/getcode/model/Feature.kt +++ b/api/src/main/java/com/getcode/model/Feature.kt @@ -17,13 +17,18 @@ data class TipCardFeature( override val available: Boolean = true, // always available ): Feature -data class TipChatFeature( - override val enabled: Boolean = BetaOptions.Defaults.tipsChatEnabled, +data class TipCardOnHomeScreenFeature( + override val enabled: Boolean = BetaOptions.Defaults.tipCardOnHomeScreen, override val available: Boolean = true, // always available ): Feature -data class TipChatCashFeature( - override val enabled: Boolean = BetaOptions.Defaults.tipsChatCashEnabled, +data class ConversationsFeature( + override val enabled: Boolean = BetaOptions.Defaults.conversationsEnabled, + override val available: Boolean = true, // always available +): Feature + +data class ConversationCashFeature( + override val enabled: Boolean = BetaOptions.Defaults.conversationCashEnabled, override val available: Boolean = true, // always available ): Feature diff --git a/api/src/main/java/com/getcode/model/Kin.kt b/api/src/main/java/com/getcode/model/Kin.kt index c5f3e3d6f..cfd3f0a3f 100644 --- a/api/src/main/java/com/getcode/model/Kin.kt +++ b/api/src/main/java/com/getcode/model/Kin.kt @@ -63,10 +63,13 @@ data class Kin(val quarks: Long): Value { } } -fun min(a: Kin, b: Kin): Kin { +private fun min(a: Kin, b: Kin): Kin { if (a.quarks > b.quarks) { return b } return a -} \ No newline at end of file +} + +val Kin.description: String + get() = "K ${toKinTruncating().quarks} ${fractionalQuarks()}" \ No newline at end of file diff --git a/api/src/main/java/com/getcode/model/PrefBool.kt b/api/src/main/java/com/getcode/model/PrefBool.kt index 0b0f44bff..185daa3ac 100644 --- a/api/src/main/java/com/getcode/model/PrefBool.kt +++ b/api/src/main/java/com/getcode/model/PrefBool.kt @@ -25,6 +25,7 @@ sealed class PrefsBool(val value: String) { data object IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_give_first_kin_airdrop"), InternalRouting data object HAS_REMOVED_LOCAL_CURRENCY: PrefsBool("removed_local_currency"), InternalRouting data object SEEN_TIP_CARD : PrefsBool("seen_tip_card"), InternalRouting + data object STARTED_TIP_CONNECT: PrefsBool("started_tip_connect"), InternalRouting data object BUY_MODULE_AVAILABLE : PrefsBool("buy_module_available"), InternalRouting @@ -43,14 +44,14 @@ sealed class PrefsBool(val value: String) { data object SHOW_CONNECTIVITY_STATUS: PrefsBool("debug_no_network"), BetaFlag data object GIVE_REQUESTS_ENABLED: PrefsBool("give_requests_enabled"), BetaFlag data object BUY_MODULE_ENABLED : PrefsBool("buy_kin_enabled"), BetaFlag - - data object CHAT_UNSUB_ENABLED: PrefsBool("chat_unsub_enabled"), BetaFlag data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag - data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag - data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag + data object CONVERSATIONS_ENABLED: PrefsBool("conversations_enabled"), BetaFlag + data object CONVERSATION_CASH_ENABLED: PrefsBool("convo_cash_enabled"), BetaFlag data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag data object KADO_WEBVIEW_ENABLED : PrefsBool("kado_inapp_enabled"), BetaFlag + data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag + data object TIP_CARD_ON_HOMESCREEN: PrefsBool("tip_card_on_home_screen"), BetaFlag } val APP_SETTINGS: List = listOf(PrefsBool.CAMERA_START_BY_DEFAULT, PrefsBool.REQUIRE_BIOMETRICS) \ No newline at end of file diff --git a/api/src/main/java/com/getcode/model/chat/Chat.kt b/api/src/main/java/com/getcode/model/chat/Chat.kt index 944af211d..624fe8893 100644 --- a/api/src/main/java/com/getcode/model/chat/Chat.kt +++ b/api/src/main/java/com/getcode/model/chat/Chat.kt @@ -2,6 +2,7 @@ package com.getcode.model.chat import com.getcode.model.Cursor import com.getcode.model.ID +import kotlinx.serialization.Serializable import java.util.UUID /** @@ -18,6 +19,7 @@ import java.util.UUID * @param cursor [Cursor] value for this chat for reference in subsequent GetChatsRequest * @param messages List of messages within this chat */ +@Serializable data class Chat( val id: ID, val type: ChatType, @@ -135,6 +137,9 @@ data class Chat( val Chat.isV2: Boolean get() = members.isNotEmpty() +val Chat.isNotification: Boolean + get() = type == ChatType.Notification + val Chat.isConversation: Boolean get() = type == ChatType.TwoWay diff --git a/api/src/main/java/com/getcode/model/chat/ChatMessage.kt b/api/src/main/java/com/getcode/model/chat/ChatMessage.kt index 752d375e4..977f3db87 100644 --- a/api/src/main/java/com/getcode/model/chat/ChatMessage.kt +++ b/api/src/main/java/com/getcode/model/chat/ChatMessage.kt @@ -4,6 +4,8 @@ import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.model.Cursor import com.getcode.model.ID import com.getcode.model.MessageStatus +import com.getcode.utils.serializer.UUIDSerializer +import kotlinx.serialization.Serializable import java.util.UUID /** @@ -18,8 +20,10 @@ import java.util.UUID * @param contents Ordered message content. A message may have more than one piece of content. * @param status Derived [MessageStatus] from [Pointer]'s in [ChatMember]. */ +@Serializable data class ChatMessage( val id: ID, // time based UUID in v2 + @Serializable(with = UUIDSerializer::class) val senderId: UUID?, val isFromSelf: Boolean, val cursor: Cursor, diff --git a/api/src/main/java/com/getcode/network/BalanceController.kt b/api/src/main/java/com/getcode/network/BalanceController.kt index c1aff4af5..74905e397 100644 --- a/api/src/main/java/com/getcode/network/BalanceController.kt +++ b/api/src/main/java/com/getcode/network/BalanceController.kt @@ -13,6 +13,7 @@ import com.getcode.solana.organizer.Organizer import com.getcode.solana.organizer.Tray import com.getcode.utils.FormatUtils import com.getcode.utils.network.NetworkConnectivityListener +import com.getcode.utils.network.retryable import com.getcode.utils.trace import io.reactivex.rxjava3.core.Completable import kotlinx.coroutines.CoroutineScope @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -38,8 +40,7 @@ data class BalanceDisplay( val marketValue: Double = 0.0, val formattedValue: String = "", val currency: Currency? = null, - - ) +) open class BalanceController @Inject constructor( exchange: Exchange, @@ -65,25 +66,34 @@ open class BalanceController @Inject constructor( .stateIn(scope, SharingStarted.Eagerly, BalanceDisplay()) init { - combine( - exchange.observeLocalRate() - .flowOn(Dispatchers.IO) - .onEach { - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = display.copy(currency = getCurrencyFromCode(it.currency)) + networkObserver.state + .map { it.connected } + .onEach { connected -> + if (connected) { + retryable({ fetchBalanceSuspend() }) + } + } + .flatMapLatest { + combine( + exchange.observeLocalRate() + .flowOn(Dispatchers.IO) + .onEach { + val display = _balanceDisplay.value ?: BalanceDisplay() + _balanceDisplay.value = + display.copy(currency = getCurrencyFromCode(it.currency)) + } + .onEach { exchange.fetchRatesIfNeeded() }, + balanceRepository.balanceFlow, + ) { rate, balance -> + rate to balance.coerceAtLeast(0.0) + }.map { (rate, balance) -> + refreshBalance(balance, rate) } - .onEach { exchange.fetchRatesIfNeeded() }, - balanceRepository.balanceFlow, - networkObserver.state - ) { rate, balance, _ -> - rate to balance.coerceAtLeast(0.0) - }.map { (rate, balance) -> - refreshBalance(balance, rate) - }.distinctUntilChanged().onEach { (marketValue, amountText) -> - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = - display.copy(marketValue = marketValue, formattedValue = amountText) - }.launchIn(scope) + }.distinctUntilChanged().onEach { (marketValue, amountText) -> + val display = _balanceDisplay.value ?: BalanceDisplay() + _balanceDisplay.value = + display.copy(marketValue = marketValue, formattedValue = amountText) + }.launchIn(scope) } fun setTray(organizer: Organizer, tray: Tray) { @@ -155,6 +165,7 @@ open class BalanceController @Inject constructor( suspend fun fetchBalanceSuspend() { + Timber.d("fetching balance") if (SessionManager.isAuthenticated() != true) { Timber.d("FetchBalance - Not authenticated") return diff --git a/api/src/main/java/com/getcode/network/HistoryController.kt b/api/src/main/java/com/getcode/network/ChatHistoryController.kt similarity index 91% rename from api/src/main/java/com/getcode/network/HistoryController.kt rename to api/src/main/java/com/getcode/network/ChatHistoryController.kt index 0e1eee91f..77d520c1d 100644 --- a/api/src/main/java/com/getcode/network/HistoryController.kt +++ b/api/src/main/java/com/getcode/network/ChatHistoryController.kt @@ -17,13 +17,12 @@ import com.getcode.model.Cursor import com.getcode.model.ID import com.getcode.model.MessageStatus import com.getcode.model.chat.ChatMember -import com.getcode.model.chat.ChatType import com.getcode.model.chat.Identity import com.getcode.model.chat.Platform import com.getcode.model.chat.Title import com.getcode.model.chat.isConversation +import com.getcode.model.chat.isNotification import com.getcode.model.chat.selfId -import com.getcode.model.description import com.getcode.network.client.Client import com.getcode.network.client.advancePointer import com.getcode.network.client.fetchChats @@ -41,31 +40,35 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import okhttp3.internal.toImmutableList import timber.log.Timber import java.util.Locale import javax.inject.Inject import javax.inject.Singleton -// TODO: See if we can merge this into [ChatController] @Singleton -class HistoryController @Inject constructor( +class ChatHistoryController @Inject constructor( private val client: Client, - private val resources: ResourceHelper, private val tipController: TipController, private val conversationMapper: ConversationMapper, private val conversationMessageMapper: ConversationMessageMapper, ) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - private val _chats = MutableStateFlow?>(null) + private val chatEntries = MutableStateFlow?>(null) + val notifications: StateFlow?> + get() = chatEntries + .map { it?.filter { entry -> entry.isNotification } } + .stateIn(this, SharingStarted.Eagerly, emptyList()) + val chats: StateFlow?> - get() = _chats.asStateFlow() + get() = chatEntries + .map { it?.filter { entry -> entry.isConversation } } + .stateIn(this, SharingStarted.Eagerly, emptyList()) var loadingMessages: Boolean = false @@ -85,9 +88,9 @@ class HistoryController @Inject constructor( pagerMap[chatId] ?: ChatMessagePagingSource( client = client, owner = owner()!!, - chat = _chats.value?.find { it.id == chatId }, + chat = chatEntries.value?.find { it.id == chatId }, onMessagesFetched = { messages -> - val chat = _chats.value?.find { it.id == chatId } ?: return@ChatMessagePagingSource + val chat = chatEntries.value?.find { it.id == chatId } ?: return@ChatMessagePagingSource updateChatWithMessages(chat, messages) } ).also { @@ -98,14 +101,14 @@ class HistoryController @Inject constructor( fun updateChatWithMessages(chat: Chat, messages: List) { val updatedMessages = (chat.messages + messages).distinctBy { it.id } val updatedChat = chat.copy(messages = updatedMessages) - val chats = _chats.value?.map { + val chats = chatEntries.value?.map { if (it.id == updatedChat.id) { updatedChat } else { it } }?.sortedByDescending { it.lastMessageMillis } - _chats.update { chats } + chatEntries.update { chats } } fun chatFlow(chatId: ID) = @@ -131,7 +134,7 @@ class HistoryController @Inject constructor( if (!update) { pagerMap.clear() chatFlows.clear() - _chats.value = containers + chatEntries.value = containers loadingMessages = true } @@ -150,13 +153,13 @@ class HistoryController @Inject constructor( } loadingMessages = false - _chats.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis } + chatEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis } } suspend fun advanceReadPointer(chatId: ID) { val owner = owner() ?: return - _chats.update { + chatEntries.update { it?.toMutableList()?.apply chats@{ indexOfFirst { chat -> chat.id == chatId } .takeIf { index -> index >= 0 } @@ -179,7 +182,7 @@ class HistoryController @Inject constructor( } fun advanceReadPointerUpTo(chatId: ID, timestamp: Long) { - _chats.update { + chatEntries.update { it?.toMutableList()?.apply chats@{ indexOfFirst { chat -> chat.id == chatId } .takeIf { index -> index >= 0 } @@ -197,7 +200,7 @@ class HistoryController @Inject constructor( suspend fun setMuted(chat: Chat, muted: Boolean): Result { val owner = owner() ?: return Result.failure(Throwable("No owner detected")) - _chats.update { + chatEntries.update { it?.toMutableList()?.apply chats@{ indexOfFirst { item -> item.id == chat.id } .takeIf { index -> index >= 0 } @@ -215,7 +218,7 @@ class HistoryController @Inject constructor( suspend fun setSubscribed(chat: Chat, subscribed: Boolean): Result { val owner = owner() ?: return Result.failure(Throwable("No owner detected")) - _chats.update { + chatEntries.update { it?.toMutableList()?.apply chats@{ indexOfFirst { item -> item.id == chat.id } .takeIf { index -> index >= 0 } @@ -249,7 +252,6 @@ class HistoryController @Inject constructor( MessageStatus.Delivered ) } - } .onFailure { Timber.e(t = it, "Failed to fetch messages for $encodedId.") diff --git a/api/src/main/java/com/getcode/network/ConversationController.kt b/api/src/main/java/com/getcode/network/ConversationController.kt index 77b866944..0e0dfe017 100644 --- a/api/src/main/java/com/getcode/network/ConversationController.kt +++ b/api/src/main/java/com/getcode/network/ConversationController.kt @@ -47,7 +47,7 @@ interface ConversationController { } class ConversationStreamController @Inject constructor( - private val historyController: HistoryController, + private val historyController: ChatHistoryController, private val exchange: Exchange, private val chatService: ChatServiceV2, private val conversationMapper: ConversationMapper, diff --git a/api/src/main/java/com/getcode/network/TipController.kt b/api/src/main/java/com/getcode/network/TipController.kt index 0cbfb92fa..fa8735d51 100644 --- a/api/src/main/java/com/getcode/network/TipController.kt +++ b/api/src/main/java/com/getcode/network/TipController.kt @@ -1,8 +1,5 @@ package com.getcode.network -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner import com.getcode.manager.SessionManager import com.getcode.model.CodePayload import com.getcode.model.PrefsBool @@ -11,6 +8,7 @@ import com.getcode.model.TipMetadata import com.getcode.model.TwitterUser import com.getcode.network.client.Client import com.getcode.network.client.fetchTwitterUser +import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.PrefRepository import com.getcode.network.repository.TwitterUserFetchError import com.getcode.network.repository.base58 @@ -44,8 +42,13 @@ typealias TipUser = Pair @Singleton class TipController @Inject constructor( private val client: Client, + betaFlags: BetaFlagsRepository, private val prefRepository: PrefRepository, -): LifecycleEventObserver { +) { + + companion object { + private const val POLL_FREQUENCY_LOOKING_SECS = 5L + } private var pollTimer: Timer? = null private var lastPoll: Long = 0L @@ -67,28 +70,40 @@ class TipController @Inject constructor( initialValue = null ) + val verificationInProgress: StateFlow = prefRepository.observeOrDefault(PrefsBool.STARTED_TIP_CONNECT, false) + .distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = false + ) + val showTwitterSplat: Flow = combine( connectedAccount, - prefRepository.observeOrDefault(PrefsBool.TIPS_ENABLED, false), + betaFlags.observe().map { it.tipsEnabled }, prefRepository.observeOrDefault(PrefsBool.SEEN_TIP_CARD, false) ) { connected, tipsEnabled, seen -> connected != null && !seen && tipsEnabled } private fun startPollTimer() { + if (connectedAccount.value != null) return + Timber.d("twitter poll start") pollTimer?.cancel() - pollTimer = fixedRateTimer("twitterPollTimer", false, 0, 1000 * 20) { - scope.launch { - val time = System.currentTimeMillis() - val isPastThrottle = time - lastPoll > 1000 * 10 || lastPoll == 0L - - if (isPastThrottle) { - callForConnectedUser() + pollTimer = + fixedRateTimer("twitterPollTimer", false, 0, 1000 * POLL_FREQUENCY_LOOKING_SECS) { + scope.launch { + val time = System.currentTimeMillis() + val isPastThrottle = + time - lastPoll > 1000 * (POLL_FREQUENCY_LOOKING_SECS / 2.0) || lastPoll == 0L + + if (isPastThrottle) { + callForConnectedUser() + } } } - } } private suspend fun callForConnectedUser() { @@ -100,6 +115,7 @@ class TipController @Inject constructor( .onSuccess { Timber.d("current user twitter connected @ ${it.username}") prefRepository.set(PrefsString.KEY_TIP_ACCOUNT, Json.encodeToString(it)) + stopTimer() } .onFailure { when (it) { @@ -120,6 +136,12 @@ class TipController @Inject constructor( } fun checkForConnection() { + if (!verificationInProgress.value) { + scope.launch { + callForConnectedUser() + } + return + } startPollTimer() } @@ -144,6 +166,7 @@ class TipController @Inject constructor( fun clearTwitterSplat() { prefRepository.set(PrefsBool.SEEN_TIP_CARD, true) + endVerification() } fun generateTipVerification(): String? { @@ -167,15 +190,20 @@ class TipController @Inject constructor( return null } - private fun stopTimer() { - pollTimer?.cancel() + fun startVerification() { + prefRepository.set(PrefsBool.STARTED_TIP_CONNECT, true) } - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - when (event) { - Lifecycle.Event.ON_RESUME -> checkForConnection() - Lifecycle.Event.ON_STOP -> stopTimer() - else -> Unit - } + private fun endVerification() { + prefRepository.set(PrefsBool.STARTED_TIP_CONNECT, false) + } + + fun stopTimer() { + stopTimerInternal() + } + + private fun stopTimerInternal() { + Timber.d("twitter poll stop") + pollTimer?.cancel() } } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/api/CurrencyApi.kt b/api/src/main/java/com/getcode/network/api/CurrencyApi.kt index b9ed682e8..cd433cb1d 100644 --- a/api/src/main/java/com/getcode/network/api/CurrencyApi.kt +++ b/api/src/main/java/com/getcode/network/api/CurrencyApi.kt @@ -7,16 +7,18 @@ import io.grpc.ManagedChannel import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import javax.inject.Inject class CurrencyApi @Inject constructor( managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io() ) : GrpcApi(managedChannel) { private val api = CurrencyGrpc.newStub(managedChannel) - fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Single = + fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Flow = api::getAllRates - .callAsSingle(request) - .subscribeOn(scheduler) + .callAsCancellableFlow(request) + .flowOn(Dispatchers.IO) } diff --git a/api/src/main/java/com/getcode/network/client/Client_Chat.kt b/api/src/main/java/com/getcode/network/client/Client_Chat.kt index 74628a913..783a63ead 100644 --- a/api/src/main/java/com/getcode/network/client/Client_Chat.kt +++ b/api/src/main/java/com/getcode/network/client/Client_Chat.kt @@ -30,14 +30,14 @@ suspend fun Client.fetchChats(owner: KeyPair): Result> { .onSuccess { Timber.d("v2 chats fetched=${it.count()}") }.onFailure { - trace("Failed fetching chats from V2", error = it, type = TraceType.Error) + trace("Failed fetching chats from V2", type = TraceType.Error) } val v1Chats = chatServiceV1.fetchChats(owner) .onSuccess { Timber.d("v1 chats fetched=${it.count()}") }.onFailure { - trace("Failed fetching chats from V1", error = it, type = TraceType.Error) + trace("Failed fetching chats from V1", type = TraceType.Error) } if (v2Chats.isSuccess || v1Chats.isSuccess) { diff --git a/api/src/main/java/com/getcode/network/client/Client_Transaction.kt b/api/src/main/java/com/getcode/network/client/Client_Transaction.kt index 3c3b8053e..637c0af1c 100644 --- a/api/src/main/java/com/getcode/network/client/Client_Transaction.kt +++ b/api/src/main/java/com/getcode/network/client/Client_Transaction.kt @@ -24,13 +24,16 @@ import com.getcode.model.intents.IntentPublicTransfer import com.getcode.model.intents.IntentRemoteSend import com.getcode.model.intents.SwapIntent import com.getcode.network.repository.TransactionRepository +import com.getcode.network.repository.WithdrawException import com.getcode.network.repository.initiateSwap import com.getcode.solana.keys.PublicKey import com.getcode.solana.keys.base58 import com.getcode.solana.organizer.GiftCardAccount import com.getcode.solana.organizer.Organizer import com.getcode.solana.organizer.Relationship +import com.getcode.utils.TraceType import com.getcode.utils.flowInterval +import com.getcode.utils.trace import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers @@ -257,11 +260,11 @@ fun Client.withdrawExternally( destination: PublicKey ): Completable { if (amount.kin.fractionalQuarks().quarks != 0L) { - throw TransactionRepository.WithdrawException.InvalidFractionalKinAmountException() + throw WithdrawException.InvalidFractionalKinAmountException() } if (amount.kin > organizer.availableBalance) { - throw TransactionRepository.WithdrawException.InsufficientFundsException() + throw WithdrawException.InsufficientFundsException() } val intent = PublicKey.generate() @@ -336,15 +339,20 @@ fun Client.withdrawExternally( Completable.complete() }.doOnComplete { Timber.d(steps.joinToString("\n")) - } + }.concatWith( // 6. Execute withdrawal - .concatWith( - withdraw( - amount = amount, - organizer = organizer, - destination = destination - ) + withdraw( + amount = amount, + organizer = organizer, + destination = destination ) + ).doOnComplete { + trace( + tag = "Trx", + message = "Withdraw completed", + type = TraceType.Process + ) + } } private fun Client.withdraw( @@ -516,6 +524,13 @@ fun Client.receiveFromPrimaryIfWithinLimits(organizer: Organizer): Completable { } } .ignoreElement() + .andThen { + trace( + tag = "Trx", + message = "Received from primary", + type = TraceType.Process + ) + } .andThen { fetchLimits(true) } } diff --git a/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt b/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt index 5050fcd9c..48764a281 100644 --- a/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt +++ b/api/src/main/java/com/getcode/network/client/TransactionReceiver.kt @@ -76,8 +76,24 @@ class TransactionReceiver @Inject constructor( organizer = organizer ).blockingGet() + trace( + tag = "Trx", + message = "Received from relationship", + type = TraceType.Process, + metadata = { + "domain" to relationship.domain.relationshipHost + "kin" to relationship.partialBalance + } + ) + receivedTotal += relationship.partialBalance + trace( + tag = "Trx", + message = "Received from incoming", + type = TraceType.Process + ) + if (intent is IntentPublicTransfer) { setTray(organizer, intent.resultTray) } diff --git a/api/src/main/java/com/getcode/network/exchange/Exchange.kt b/api/src/main/java/com/getcode/network/exchange/Exchange.kt index f9283db33..c2c6d4242 100644 --- a/api/src/main/java/com/getcode/network/exchange/Exchange.kt +++ b/api/src/main/java/com/getcode/network/exchange/Exchange.kt @@ -9,8 +9,12 @@ import com.getcode.model.Rate import com.getcode.network.api.CurrencyApi import com.getcode.network.core.NetworkOracle import com.getcode.network.repository.PrefRepository +import com.getcode.network.service.ApiRateResult +import com.getcode.network.service.CurrencyService import com.getcode.utils.ErrorUtils import com.getcode.utils.TraceType +import com.getcode.utils.format +import com.getcode.utils.network.retryable import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,10 +27,12 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.datetime.Instant import timber.log.Timber import java.util.Date import javax.inject.Inject import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException import kotlin.time.Duration.Companion.convert import kotlin.time.Duration.Companion.minutes import kotlin.time.DurationUnit @@ -86,8 +92,7 @@ class ExchangeNull : Exchange { } class CodeExchange @Inject constructor( - private val currencyApi: CurrencyApi, - private val networkOracle: NetworkOracle, + private val currencyService: CurrencyService, prefs: PrefRepository, private val preferredCurrency: suspend () -> Currency?, private val defaultCurrency: suspend () -> Currency?, @@ -124,6 +129,7 @@ class CodeExchange @Inject constructor( private val isStale: Boolean get() { + if (rates.rates.isEmpty()) return true // Remember, the exchange rates date is the server-provided // date-of-rate and not the time the rate was fetched. It // might be reasonable for the server to return a date that @@ -143,7 +149,7 @@ class CodeExchange @Inject constructor( .mapNotNull { preferred -> preferred ?: CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) }.onEach { setEntryCurrency(it) } - .launchIn(this) + .launchIn(this@CodeExchange) } launch { @@ -167,22 +173,26 @@ class CodeExchange @Inject constructor( override suspend fun fetchRatesIfNeeded() { if (isStale) { - runCatching { fetchExchangeRates() } - .onSuccess { (updatedRates, date) -> - db?.exchangeDao()?.insert(rates = updatedRates, syncedAt = date) - set(RatesBox(date, updatedRates)) - }.onFailure { - it.printStackTrace() + retryable( + call = { + currencyService.getRates() + .onSuccess { (updatedRates, date) -> + db?.exchangeDao()?.insert(rates = updatedRates, syncedAt = date) + set(RatesBox(date, updatedRates)) + } } + ) } + + updateRates() } - private suspend fun setEntryCurrency(currency: CurrencyCode) { + private fun setEntryCurrency(currency: CurrencyCode) { entryCurrency = currency updateRates() } - private suspend fun setLocalCurrency(currency: CurrencyCode) { + private fun setLocalCurrency(currency: CurrencyCode) { localCurrency = currency updateRates() } @@ -209,78 +219,67 @@ class CodeExchange @Inject constructor( override fun rateForUsd(): Rate? = rates.rateForUsd() - private suspend fun updateRates() { + private fun updateRates() { if (rates.isEmpty) { return } val localRate = localCurrency?.let { rates.rateFor(it) } - _localRate.value = if (localRate != null) { - trace( - message = "Updated the local currency: $localCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Silent - ) - localRate - } else { - trace( - message = "local:: Rate for $localCurrency not found. Defaulting to USD.", - type = TraceType.Silent - ) - rates.rateForUsd()!! + val localChanged = _localRate.value != localRate + if (localChanged) { + _localRate.value = if (localRate != null) { + trace( + tag = "Background", + message = "Updated the local currency: $localCurrency, " + + "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + + "Date: ${Date(rates.dateMillis)}", + type = TraceType.Process + ) + localRate + } else { + trace( + tag = "Background", + message = "local:: Rate for $localCurrency not found. Defaulting to USD.", + type = TraceType.Process + ) + rates.rateForUsd()!! + } } val entryRate = entryCurrency?.let { rates.rateFor(it) } - _entryRate.value = if (entryRate != null) { - trace( - message = "Updated the entry currency: $entryCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Silent - ) - entryRate - } else { - trace( - message = "entry:: Rate for $entryCurrency not found. Defaulting to USD.", - type = TraceType.Silent - ) - rates.rateForUsd()!! + val entryChanged = _entryRate.value != entryRate + if (entryChanged) { + _entryRate.value = if (entryRate != null) { + trace( + tag = "Background", + message = "Updated the entry currency: $entryCurrency, " + + "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + + "Date: ${Date(rates.dateMillis)}", + type = TraceType.Process + ) + entryRate + } else { + trace( + tag = "Background", + message = "entry:: Rate for $entryCurrency not found. Defaulting to USD.", + type = TraceType.Process + ) + rates.rateForUsd()!! + } } - } - - @OptIn(ExperimentalTime::class) - @SuppressLint("CheckResult") - private suspend fun fetchExchangeRates() = suspendCancellableCoroutine { cont -> - Timber.d("fetching rates") - currencyApi.getRates() - .let { networkOracle.managedRequest(it) } - .subscribe({ response -> - val rates = response.ratesMap.mapNotNull { (key, value) -> - val currency = CurrencyCode.tryValueOf(key) ?: return@mapNotNull null - Rate(fx = value, currency = currency) - }.toMutableList() - - if (rates.none { it.currency == CurrencyCode.KIN }) { - rates.add(Rate(fx = 1.0, currency = CurrencyCode.KIN)) + if (localChanged || entryChanged) { + trace(tag = "Background", + message = "Updated rates", + type = TraceType.Process, + metadata = { + "date" to Instant.fromEpochMilliseconds(rates.dateMillis) + .format("yyyy-MM-dd HH:mm:ss") } - - cont.resume( - rates.toList() to convert( - value = response.asOf.seconds.toDouble(), - sourceUnit = DurationUnit.SECONDS, - targetUnit = DurationUnit.MILLISECONDS - ).toLong() - ) - }, { - ErrorUtils.handleError(it) - cont.resume(emptyList() to System.currentTimeMillis()) - }) + ) + } } - - } private data class RatesBox(val dateMillis: Long, val rates: Map) { diff --git a/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt b/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt index d42ded921..7d6a9f0da 100644 --- a/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt @@ -15,10 +15,12 @@ data class BetaOptions( val buyModuleEnabled: Boolean, val chatUnsubEnabled: Boolean, val tipsEnabled: Boolean, - val tipsChatEnabled: Boolean, - val tipsChatCashEnabled: Boolean, + val conversationsEnabled: Boolean, + val conversationCashEnabled: Boolean, val balanceCurrencySelectionEnabled: Boolean, val kadoWebViewEnabled: Boolean, + val shareTweetToTip: Boolean, + val tipCardOnHomeScreen: Boolean, ) { companion object { // Default states for various beta flags in app. @@ -32,10 +34,12 @@ data class BetaOptions( buyModuleEnabled = true, chatUnsubEnabled = false, tipsEnabled = true, - tipsChatEnabled = false, - tipsChatCashEnabled = false, + conversationsEnabled = false, + conversationCashEnabled = false, balanceCurrencySelectionEnabled = true, kadoWebViewEnabled = false, + shareTweetToTip = false, + tipCardOnHomeScreen = true, ) } } @@ -66,11 +70,13 @@ class BetaFlagsRepository @Inject constructor( observeBetaFlag(PrefsBool.BUY_MODULE_ENABLED, default = defaults.buyModuleEnabled), observeBetaFlag(PrefsBool.CHAT_UNSUB_ENABLED, default = defaults.chatUnsubEnabled), observeBetaFlag(PrefsBool.TIPS_ENABLED, default = defaults.tipsEnabled), - observeBetaFlag(PrefsBool.TIPS_CHAT_ENABLED, default = defaults.tipsChatEnabled), - observeBetaFlag(PrefsBool.TIPS_CHAT_CASH_ENABLED, default = defaults.tipsChatCashEnabled), + observeBetaFlag(PrefsBool.CONVERSATIONS_ENABLED, default = defaults.conversationsEnabled), + observeBetaFlag(PrefsBool.CONVERSATION_CASH_ENABLED, default = defaults.conversationCashEnabled), observeBetaFlag(PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, defaults.balanceCurrencySelectionEnabled), observeBetaFlag(PrefsBool.DISPLAY_ERRORS, default = defaults.displayErrors), - observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled) + observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled), + observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip), + observeBetaFlag(PrefsBool.TIP_CARD_ON_HOMESCREEN, defaults.tipCardOnHomeScreen) ) { BetaOptions( showNetworkDropOff = it[0], @@ -81,11 +87,13 @@ class BetaFlagsRepository @Inject constructor( buyModuleEnabled = it[5], chatUnsubEnabled = it[6], tipsEnabled = it[7], - tipsChatEnabled = it[8], - tipsChatCashEnabled = it[9], + conversationsEnabled = it[8], + conversationCashEnabled = it[9], balanceCurrencySelectionEnabled = it[10], displayErrors = it[11], kadoWebViewEnabled = it[12], + shareTweetToTip = it[13], + tipCardOnHomeScreen = it[14] ) } } @@ -98,4 +106,8 @@ class BetaFlagsRepository @Inject constructor( b.takeIf { a } ?: default } } + + suspend fun isEnabled(flag: PrefsBool): Boolean { + return prefRepository.get(flag, false) + } } diff --git a/api/src/main/java/com/getcode/network/repository/FeatureRepository.kt b/api/src/main/java/com/getcode/network/repository/FeatureRepository.kt index 34bfd80c7..680b157fc 100644 --- a/api/src/main/java/com/getcode/network/repository/FeatureRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/FeatureRepository.kt @@ -5,8 +5,9 @@ import com.getcode.model.BuyModuleFeature import com.getcode.model.PrefsBool import com.getcode.model.RequestKinFeature import com.getcode.model.TipCardFeature -import com.getcode.model.TipChatCashFeature -import com.getcode.model.TipChatFeature +import com.getcode.model.TipCardOnHomeScreenFeature +import com.getcode.model.ConversationCashFeature +import com.getcode.model.ConversationsFeature import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -15,7 +16,7 @@ import javax.inject.Inject * Collates [BetaOptions] with server availability (stored in [PrefRepository]). */ class FeatureRepository @Inject constructor( - betaFlags: BetaFlagsRepository, + private val betaFlags: BetaFlagsRepository, prefRepository: PrefRepository, ) { val buyModule = combine( @@ -24,11 +25,13 @@ class FeatureRepository @Inject constructor( ) { enabled, available -> BuyModuleFeature(enabled, available) } val tipCards = betaFlags.observe().map { TipCardFeature(it.tipsEnabled) } - - val tipChat = betaFlags.observe().map { TipChatFeature(it.tipsChatEnabled) } - val tipChatCash = betaFlags.observe().map { TipChatCashFeature(it.tipsChatCashEnabled) } + val tipCardOnHomeScreen = betaFlags.observe().map { TipCardOnHomeScreenFeature(it.tipCardOnHomeScreen) } + val conversations = betaFlags.observe().map { ConversationsFeature(it.conversationsEnabled) } + val conversationsCash = betaFlags.observe().map { ConversationCashFeature(it.conversationCashEnabled) } val requestKin = betaFlags.observe().map { RequestKinFeature(it.giveRequestsEnabled) } val balanceCurrencySelection = betaFlags.observe().map { BalanceCurrencyFeature(it.balanceCurrencySelectionEnabled) } + + suspend fun isEnabled(feature: PrefsBool): Boolean = betaFlags.isEnabled(feature) } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt b/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt index 6b0b0a225..644b6d912 100644 --- a/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/IdentityRepository.kt @@ -389,7 +389,6 @@ class IdentityRepository @Inject constructor( } }.first() } catch (e: Exception) { - e.printStackTrace() ErrorUtils.handleError(e) Result.failure(e) } diff --git a/api/src/main/java/com/getcode/network/repository/MessagingRepository.kt b/api/src/main/java/com/getcode/network/repository/MessagingRepository.kt index b2d256782..e4217e4bc 100644 --- a/api/src/main/java/com/getcode/network/repository/MessagingRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/MessagingRepository.kt @@ -171,6 +171,7 @@ class MessagingRepository @Inject constructor( .build() return networkOracle.managedRequest(messagingApi.pollMessages(request)) + .observeOn(Schedulers.io()) .map { response -> Timber.d("response=${response.messagesList}") response.messagesList.mapNotNull { m -> StreamMessage.getInstance(m) } diff --git a/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt b/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt index 795aad651..7ad1f295c 100644 --- a/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt @@ -18,10 +18,12 @@ import com.getcode.network.exchange.Exchange import com.getcode.solana.keys.PublicKey import com.getcode.utils.ErrorUtils import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.resume @@ -90,7 +92,9 @@ class PaymentRepository @Inject constructor( @SuppressLint("CheckResult") suspend fun completePayment(amount: KinAmount, rendezvousKey: KeyPair) { // 1. ensure we have exchange rates and compute the fees for this transaction - exchange.fetchRatesIfNeeded() + withContext(Dispatchers.IO) { + exchange.fetchRatesIfNeeded() + } var paymentAmount = amount return suspendCancellableCoroutine { cont -> @@ -156,7 +160,7 @@ class PaymentRepository @Inject constructor( Completable.concatArray( balanceController.fetchBalance(), client.fetchLimits(isForce = true) - ).doOnComplete { + ).observeOn(Schedulers.io()).doOnComplete { analytics.transfer( amount = paymentAmount, successful = true, @@ -209,7 +213,7 @@ class PaymentRepository @Inject constructor( Completable.concatArray( balanceController.fetchBalance(), client.fetchLimits(isForce = true) - ).doOnComplete { + ).observeOn(Schedulers.io()).doOnComplete { analytics.transferForTip(amount = amount, successful = true) cont.resume(Unit) }.subscribe() diff --git a/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt b/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt index 7c748e7cf..9a65b20d7 100644 --- a/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/TransactionRepository.kt @@ -42,6 +42,7 @@ import com.getcode.solana.organizer.GiftCardAccount import com.getcode.solana.organizer.Organizer import com.getcode.solana.organizer.Relationship import com.getcode.utils.ErrorUtils +import com.getcode.utils.TraceType import com.getcode.utils.bytes import com.getcode.utils.trace import com.google.protobuf.Timestamp @@ -303,8 +304,8 @@ class TransactionRepository @Inject constructor( val subject = SingleSubject.create() var serverMessageStream: StreamObserver? = null - val serverResponse = object : StreamObserver { - override fun onNext(value: TransactionService.SubmitIntentResponse?) { + val serverResponse = object : StreamObserver { + override fun onNext(value: SubmitIntentResponse?) { Timber.i("Received: " + value?.responseCase?.name.orEmpty()) when (value?.responseCase) { @@ -373,8 +374,9 @@ class TransactionRepository @Inject constructor( else -> Unit } - Timber.e( - "Error: ${errors.joinToString("\n")}" + trace( + "Error: ${errors.joinToString("\n")}", + type = TraceType.Error ) } @@ -582,6 +584,17 @@ class TransactionRepository @Inject constructor( .doOnSuccess { setLimits(it) setMaximumDeposit(it.maxDeposit) + trace( + tag = "Trx", + message = "Fetched limits", + type = TraceType.Process, + metadata = { + val sendLimit = it.sendLimitFor(CurrencyCode.USD) + if (sendLimit != null) { + "limitNextTx" to sendLimit + } + } + ) } .doOnError(ErrorUtils::handleError) .toFlowable() @@ -696,90 +709,90 @@ class TransactionRepository @Inject constructor( } } } +} - class ErrorSubmitIntentException( - val errorSubmitIntent: ErrorSubmitIntent, - cause: Throwable? = null, - val messageString: String = "" - ) : Exception(cause) { - override val message: String - get() = "${errorSubmitIntent.javaClass.simpleName} $messageString" - } +class ErrorSubmitIntentException( + val errorSubmitIntent: ErrorSubmitIntent, + cause: Throwable? = null, + val messageString: String = "" +) : Exception(cause) { + override val message: String + get() = "${errorSubmitIntent.javaClass.simpleName} $messageString" +} - enum class DeniedReason { - Unspecified, - TooManyFreeAccountsForPhoneNumber, - TooManyFreeAccountsForDevice, - UnsupportedCountry, - UnsupportedDevice; +enum class DeniedReason { + Unspecified, + TooManyFreeAccountsForPhoneNumber, + TooManyFreeAccountsForDevice, + UnsupportedCountry, + UnsupportedDevice; - companion object { - fun fromValue(value: Int): DeniedReason { - return entries.firstOrNull { it.ordinal == value } ?: Unspecified - } + companion object { + fun fromValue(value: Int): DeniedReason { + return entries.firstOrNull { it.ordinal == value } ?: Unspecified } } +} - sealed class ErrorSubmitIntent(val value: Int) { - data class Denied(val reasons: List = emptyList()): ErrorSubmitIntent(0) - data class InvalidIntent(val reasons: List): ErrorSubmitIntent(1) - data object SignatureError: ErrorSubmitIntent(2) - data class StaleState(val reasons: List): ErrorSubmitIntent(3) - data object Unknown: ErrorSubmitIntent(-1) - data object DeviceTokenUnavailable: ErrorSubmitIntent(-2) - - override fun toString(): String { - return when (this) { - is Denied -> "denied(${reasons.joinToString()})" - DeviceTokenUnavailable -> "deviceTokenUnavailable" - is InvalidIntent -> "invalidIntent(${reasons.joinToString()})" - SignatureError -> "signatureError" - is StaleState -> "staleState(${reasons.joinToString()})" - Unknown -> "unknown" - } +sealed class ErrorSubmitIntent(val value: Int) { + data class Denied(val reasons: List = emptyList()): ErrorSubmitIntent(0) + data class InvalidIntent(val reasons: List): ErrorSubmitIntent(1) + data object SignatureError: ErrorSubmitIntent(2) + data class StaleState(val reasons: List): ErrorSubmitIntent(3) + data object Unknown: ErrorSubmitIntent(-1) + data object DeviceTokenUnavailable: ErrorSubmitIntent(-2) + + override fun toString(): String { + return when (this) { + is Denied -> "denied(${reasons.joinToString()})" + DeviceTokenUnavailable -> "deviceTokenUnavailable" + is InvalidIntent -> "invalidIntent(${reasons.joinToString()})" + SignatureError -> "signatureError" + is StaleState -> "staleState(${reasons.joinToString()})" + Unknown -> "unknown" } + } - companion object { - operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent { - val reasonStrings = proto.errorDetailsList.mapNotNull { - when (it.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> - it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() } - else -> null - } + companion object { + operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent { + val reasonStrings = proto.errorDetailsList.mapNotNull { + when (it.typeCase) { + TransactionService.ErrorDetails.TypeCase.REASON_STRING -> + it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() } + else -> null } - return when (proto.code) { - SubmitIntentResponse.Error.Code.DENIED -> { - val reasons = proto.errorDetailsList.mapNotNull { - if (!it.hasIntentDenied()) return@mapNotNull null - DeniedReason.fromValue(it.intentDenied.reasonValue) - } - - Denied(reasons) + } + return when (proto.code) { + SubmitIntentResponse.Error.Code.DENIED -> { + val reasons = proto.errorDetailsList.mapNotNull { + if (!it.hasIntentDenied()) return@mapNotNull null + DeniedReason.fromValue(it.intentDenied.reasonValue) } - SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings) - SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError - SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings) - SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown - else -> return Unknown + + Denied(reasons) } + SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings) + SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError + SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings) + SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown + else -> return Unknown } } } +} - sealed class WithdrawException : Exception() { - class InvalidFractionalKinAmountException : WithdrawException() - class InsufficientFundsException : WithdrawException() - } +sealed class WithdrawException : Exception() { + class InvalidFractionalKinAmountException : WithdrawException() + class InsufficientFundsException : WithdrawException() +} - sealed class FetchUpgradeableIntentsException : Exception() { - class DeserializationException : FetchUpgradeableIntentsException() - class UnknownException : FetchUpgradeableIntentsException() - } +sealed class FetchUpgradeableIntentsException : Exception() { + class DeserializationException : FetchUpgradeableIntentsException() + class UnknownException : FetchUpgradeableIntentsException() +} - sealed class AirdropException : Exception() { - class AlreadyClaimedException : AirdropException() - class UnavailableException : AirdropException() - class UnknownException : AirdropException() - } +sealed class AirdropException : Exception() { + class AlreadyClaimedException : AirdropException() + class UnavailableException : AirdropException() + class UnknownException : AirdropException() } diff --git a/api/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt b/api/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt index ec384d551..dc5d8f177 100644 --- a/api/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt +++ b/api/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt @@ -8,7 +8,6 @@ import com.getcode.model.intents.SwapConfigParameters import com.getcode.model.intents.SwapIntent import com.getcode.model.intents.requestToSubmitSignatures import com.getcode.network.core.BidirectionalStreamReference -import com.getcode.network.repository.TransactionRepository.ErrorSubmitIntent import com.getcode.solana.SolanaTransaction import com.getcode.solana.diff import com.getcode.solana.keys.Signature @@ -17,7 +16,6 @@ import com.getcode.solana.organizer.Organizer import com.getcode.utils.ErrorUtils import io.grpc.stub.StreamObserver import kotlinx.coroutines.suspendCancellableCoroutine -import org.kin.sdk.base.tools.Base58 import timber.log.Timber import kotlin.coroutines.resume diff --git a/api/src/main/java/com/getcode/network/service/CurrencyService.kt b/api/src/main/java/com/getcode/network/service/CurrencyService.kt new file mode 100644 index 000000000..fd99cda8e --- /dev/null +++ b/api/src/main/java/com/getcode/network/service/CurrencyService.kt @@ -0,0 +1,54 @@ +package com.getcode.network.service + +import com.getcode.model.CurrencyCode +import com.getcode.model.Rate +import com.getcode.network.api.CurrencyApi +import com.getcode.network.core.NetworkOracle +import com.getcode.utils.ErrorUtils +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber +import javax.inject.Inject +import kotlin.time.Duration.Companion.convert +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime + +data class ApiRateResult( + val rates: List, + val dateMillis: Long, +) + +class CurrencyService @Inject constructor( + private val api: CurrencyApi, + private val networkOracle: NetworkOracle, +) { + @OptIn(ExperimentalTime::class) + suspend fun getRates(): Result { + Timber.d("fetching rates") + return try { + networkOracle.managedRequest(api.getRates()) + .map { response -> + val rates = response.ratesMap.mapNotNull { (key, value) -> + val currency = CurrencyCode.tryValueOf(key) ?: return@mapNotNull null + Rate(fx = value, currency = currency) + }.toMutableList() + + if (rates.none { it.currency == CurrencyCode.KIN }) { + rates.add(Rate(fx = 1.0, currency = CurrencyCode.KIN)) + } + + Result.success(ApiRateResult( + rates = rates.toList(), + dateMillis = convert( + value = response.asOf.seconds.toDouble(), + sourceUnit = DurationUnit.SECONDS, + targetUnit = DurationUnit.MILLISECONDS + ).toLong() + )) + }.first() + } catch (e: Exception) { + ErrorUtils.handleError(e) + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/getcode/solana/organizer/Organizer.kt b/api/src/main/java/com/getcode/solana/organizer/Organizer.kt index a17f0c733..ecc5fe89b 100644 --- a/api/src/main/java/com/getcode/solana/organizer/Organizer.kt +++ b/api/src/main/java/com/getcode/solana/organizer/Organizer.kt @@ -8,7 +8,9 @@ import com.getcode.model.Kin import com.getcode.model.unusable import com.getcode.network.repository.getPublicKeyBase58 import com.getcode.solana.keys.* +import com.getcode.utils.TraceType import com.getcode.utils.timedTrace +import com.getcode.utils.trace import timber.log.Timber class Organizer( @@ -59,6 +61,15 @@ class Organizer( this.accountInfos = infos tray.createRelationships(infos) propagateBalances() + + trace( + tag = "Organizer", + message = "Fetched account infos", + type = TraceType.Process, + metadata = { + "tray" to tray.reportableRepresentation() + } + ) } fun getAccountInfo() = accountInfos diff --git a/api/src/main/java/com/getcode/solana/organizer/Tray.kt b/api/src/main/java/com/getcode/solana/organizer/Tray.kt index bbda8f243..2a2888509 100644 --- a/api/src/main/java/com/getcode/solana/organizer/Tray.kt +++ b/api/src/main/java/com/getcode/solana/organizer/Tray.kt @@ -1,6 +1,5 @@ package com.getcode.solana.organizer -import android.content.Context import com.getcode.crypt.DerivePath import com.getcode.crypt.DerivedKey import com.getcode.crypt.MnemonicPhrase @@ -8,9 +7,11 @@ import com.getcode.model.AccountInfo import com.getcode.model.Domain import com.getcode.model.Kin import com.getcode.model.RelationshipBox +import com.getcode.model.description import com.getcode.solana.keys.PublicKey +import com.getcode.solana.keys.base58 import com.getcode.utils.TraceType -import com.getcode.utils.timedTrace +import com.getcode.utils.padded import com.getcode.utils.trace import kotlin.math.min @@ -866,6 +867,29 @@ class Tray( return container } + fun reportableRepresentation(): List { + return listOf( + string(named = "Primary ", partialAccount = owner), + string(named = "Incoming ", partialAccount = incoming), + string(named = "Outgoing ", partialAccount = outgoing), + string("1 ", slot = slot(SlotType.Bucket1)), + string("10 ", slot = slot(SlotType.Bucket10)), + string("100 ", slot = slot(SlotType.Bucket100)), + string("1k ", slot = slot(SlotType.Bucket1k)), + string("10k ", slot = slot(SlotType.Bucket10k)), + string("100k ", slot = slot(SlotType.Bucket100k)), + string("1m ", slot = slot(SlotType.Bucket1m)), + ) + } + + private fun string(named: String, partialAccount: PartialAccount): String { + return "$named ${partialAccount.getCluster().vaultPublicKey.base58().padded(44)}) ${partialAccount.partialBalance.description}" + } + + private fun string(named: String, slot: Slot): String { + return "$named ${slot.getCluster().vaultPublicKey.base58().padded(44)}) ${slot.partialBalance.description}" + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/api/src/main/java/com/getcode/utils/ErrorUtils.kt b/api/src/main/java/com/getcode/utils/ErrorUtils.kt index f2e98b319..a6577cd1d 100644 --- a/api/src/main/java/com/getcode/utils/ErrorUtils.kt +++ b/api/src/main/java/com/getcode/utils/ErrorUtils.kt @@ -8,6 +8,7 @@ import com.getcode.manager.TopBarManager import io.grpc.StatusRuntimeException import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException import io.reactivex.rxjava3.exceptions.UndeliverableException +import kotlinx.coroutines.TimeoutCancellationException import timber.log.Timber import java.net.ConnectException import java.net.UnknownHostException @@ -42,6 +43,7 @@ object ErrorUtils { BuildConfig.NOTIFY_ERRORS && throwable !is UnknownHostException && throwable !is TimeoutException && + throwable !is TimeoutCancellationException && throwable !is ConnectException ) { FirebaseCrashlytics.getInstance().recordException(throwable) @@ -62,7 +64,7 @@ object ErrorUtils { throwable.cause is StatusRuntimeException private fun isSuppressibleError(throwable: Throwable): Boolean = - throwable is SQLException || throwable is net.sqlcipher.SQLException || throwable is SuppressibleException + throwable is SQLException || throwable is net.sqlcipher.SQLException || throwable is SuppressibleException || throwable is TimeoutCancellationException } data class SuppressibleException(override val message: String): Throwable(message) \ No newline at end of file diff --git a/app/src/main/java/com/getcode/util/Instant.kt b/api/src/main/java/com/getcode/utils/Instant.kt similarity index 69% rename from app/src/main/java/com/getcode/util/Instant.kt rename to api/src/main/java/com/getcode/utils/Instant.kt index d31c36213..bb4a80e9a 100644 --- a/app/src/main/java/com/getcode/util/Instant.kt +++ b/api/src/main/java/com/getcode/utils/Instant.kt @@ -1,4 +1,4 @@ -package com.getcode.util +package com.getcode.utils import kotlinx.datetime.DatePeriod import kotlinx.datetime.DateTimeUnit @@ -9,6 +9,9 @@ import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.minus import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale fun Instant.toLocalDate(timeZone: TimeZone = TimeZone.currentSystemDefault()) = toLocalDateTime(timeZone).date @@ -18,4 +21,13 @@ fun LocalDate.atStartOfDay(tz: TimeZone = TimeZone.currentSystemDefault()) = atS fun LocalDate.atEndOfDay(tz: TimeZone = TimeZone.currentSystemDefault()): Instant { val tomorrowAtMidnight = ((this + DatePeriod(days = 1)).atStartOfDayIn(tz)) return tomorrowAtMidnight.minus(value = 1, unit = DateTimeUnit.NANOSECOND) +} + +fun Instant.format(format: String = "yyyy-MM-dd"): String { + val epoch = this.toEpochMilliseconds() + + val formatter = SimpleDateFormat(format, Locale.getDefault()) + val date = Date(epoch) + + return formatter.format(date) } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/utils/Logging.kt b/api/src/main/java/com/getcode/utils/Logging.kt index be475b31d..f73a90a45 100644 --- a/api/src/main/java/com/getcode/utils/Logging.kt +++ b/api/src/main/java/com/getcode/utils/Logging.kt @@ -67,25 +67,28 @@ fun trace( message: String, tag: String? = null, type: TraceType = TraceType.Log, + metadata: MetadataBuilder.() -> Unit = {}, error: Throwable? = null ) { - val tree = if (tag == null) Timber else Timber.tag(tag) - val traceMessage = if (tag == null) message else "trace : $message" + val tagBlock = tag?.let { "[$it] " } + val tree = if (tagBlock == null) Timber else Timber.tag(tagBlock) - tree.d(traceMessage) + tree.d(message) + + val metadataMap = metadata { metadata() } if (Bugsnag.isStarted()) { val breadcrumb = if (tag != null) { - "$tag | $traceMessage" + "$tagBlock $message" } else { - traceMessage + message } val breadcrumbType = type.toBugsnagBreadcrumbType() if (breadcrumbType != null) { Bugsnag.leaveBreadcrumb( breadcrumb, - emptyMap(), + metadataMap, breadcrumbType ) } @@ -98,6 +101,7 @@ fun timedTrace( message: String, tag: String? = null, type: TraceType = TraceType.Log, + metadata: MetadataBuilder.() -> Unit = {}, error: Throwable? = null, block: () -> T ): T { @@ -107,7 +111,23 @@ fun timedTrace( } val newMessage = "$message took ${time.inWholeMilliseconds}ms" - trace(newMessage, tag, type, error) + trace(newMessage, tag, type, metadata, error) return result +} + +class MetadataBuilder { + private val map = mutableMapOf() + + infix fun String.to(value: Any) { + map[this] = value + } + + fun build(): Map = map +} + +fun metadata(block: MetadataBuilder.() -> Unit): Map { + val builder = MetadataBuilder() + builder.block() + return builder.build() } \ No newline at end of file diff --git a/api/src/main/java/com/getcode/utils/String.kt b/api/src/main/java/com/getcode/utils/String.kt index eb9404360..1180ef6f4 100644 --- a/api/src/main/java/com/getcode/utils/String.kt +++ b/api/src/main/java/com/getcode/utils/String.kt @@ -30,5 +30,15 @@ fun String.base64EncodedData(): ByteArray { return data } +fun String.padded(minCount: Int): String { + return if (this.length < minCount) { + val toInsert = minCount - this.length + val padding = " ".repeat(toInsert) + this + padding + } else { + this + } +} + typealias Base64String = String typealias Base58String = String \ No newline at end of file diff --git a/api/src/main/java/com/getcode/utils/network/Retry.kt b/api/src/main/java/com/getcode/utils/network/Retry.kt new file mode 100644 index 000000000..e5c6128e0 --- /dev/null +++ b/api/src/main/java/com/getcode/utils/network/Retry.kt @@ -0,0 +1,58 @@ +package com.getcode.utils.network + +import com.getcode.utils.TraceType +import com.getcode.utils.trace +import kotlinx.coroutines.delay +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +suspend fun retryable( + call: suspend () -> T, + maxRetries: Int = 3, + delayDuration: Duration = 2.seconds, + onRetry: (Int) -> Unit = { currentAttempt -> + trace( + message = "Retrying call", + metadata = { + "count" to currentAttempt + }, + type = TraceType.Process, + ) + }, + onError: (startTime: TimeSource.Monotonic.ValueTimeMark) -> Unit = { startTime -> + trace( + "Failed to get a success after $maxRetries attempts in ${startTime.elapsedNow().inWholeMilliseconds} ms", + type = TraceType.Error + ) + }, +): T? { + var currentAttempt = 0 + val startTime = TimeSource.Monotonic.markNow() + + while (currentAttempt < maxRetries) { + val result = try { + call() + } catch (e: Exception) { + trace( + message = "Attempt $currentAttempt failed with exception: ${e.message}", + error = e, + type = TraceType.Error + ) + null + } + + if (result != null) { + return result + } else { + currentAttempt++ + if (currentAttempt < maxRetries) { + onRetry(currentAttempt) + delay(delayDuration.inWholeMilliseconds) + } + } + } + + onError(startTime) + return null +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 572299105..f6196b92d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,6 +14,7 @@ plugins { id(Plugins.firebase_perf) id(Plugins.bugsnag) id(Plugins.secrets_gradle_plugin) + id(Plugins.versioning_gradle_plugin) } val contributorsSigningConfig = ContributorsSignatory(rootProject) @@ -24,7 +25,7 @@ android { defaultConfig { applicationId = Android.namespace - versionCode = Packaging.versionCode + versionCode = versioning.getVersionCode() versionName = Packaging.versionName minSdk = Android.minSdkVersion @@ -114,6 +115,8 @@ dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation(project(":api")) + implementation(project(":crypto:ed25519")) + implementation(project(":crypto:kin")) implementation(project(":common:resources")) implementation(project(":common:theme")) implementation(project(":vendor:tipkit:tipkit-m2")) @@ -223,5 +226,5 @@ dependencies { implementation(Libs.timber) implementation(Libs.bugsnag) - implementation("dev.chrisbanes.haze:haze:0.7.3") + implementation(Libs.haze) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8482a2b34..fb05d0053 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -56,6 +56,11 @@ } -keep public class * extends java.lang.Exception +-keep public class * extends com.getcode.network.repository.ErrorSubmitIntent +-keep public class * extends com.getcode.network.repository.ErrorSubmitIntentException +-keep public class * extends com.getcode.network.repository.WithdrawException +-keep public class * extends com.getcode.network.repository.FetchUpgradeableIntentsException +-keep public class * extends com.getcode.network.repository.AirdropException # https://github.com/firebase/firebase-android-sdk/issues/3688 -keep class org.json.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2f5209785..093d4174e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -138,8 +138,32 @@ android:scheme="codewallet" /> + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/getcode/inject/ApiModule.kt b/app/src/main/java/com/getcode/inject/ApiModule.kt index 9a7648902..f503bba84 100644 --- a/app/src/main/java/com/getcode/inject/ApiModule.kt +++ b/app/src/main/java/com/getcode/inject/ApiModule.kt @@ -31,6 +31,7 @@ import com.getcode.util.AccountAuthenticator import com.getcode.util.locale.LocaleHelper import com.getcode.utils.network.NetworkConnectivityListener import com.getcode.network.service.ChatServiceV2 +import com.getcode.network.service.CurrencyService import com.getcode.network.service.DeviceService import com.getcode.util.CurrencyUtils import com.getcode.util.resources.ResourceHelper @@ -191,14 +192,12 @@ object ApiModule { @Singleton @Provides fun providesExchange( - currencyApi: CurrencyApi, - networkOracle: NetworkOracle, + currencyService: CurrencyService, locale: LocaleHelper, currencyUtils: CurrencyUtils, prefRepository: PrefRepository, ): Exchange = CodeExchange( - currencyApi = currencyApi, - networkOracle = networkOracle, + currencyService = currencyService, prefs = prefRepository, preferredCurrency = { val preferredCurrencyCode = prefRepository.get( diff --git a/app/src/main/java/com/getcode/inject/DataModule.kt b/app/src/main/java/com/getcode/inject/DataModule.kt index e22594d62..b02ccb473 100644 --- a/app/src/main/java/com/getcode/inject/DataModule.kt +++ b/app/src/main/java/com/getcode/inject/DataModule.kt @@ -4,8 +4,7 @@ import com.getcode.mapper.ConversationMapper import com.getcode.mapper.ConversationMessageWithContentMapper import com.getcode.network.ConversationController import com.getcode.network.ConversationStreamController -import com.getcode.network.HistoryController -import com.getcode.network.client.Client +import com.getcode.network.ChatHistoryController import com.getcode.network.exchange.Exchange import com.getcode.network.service.ChatServiceV2 import dagger.Module @@ -21,7 +20,7 @@ object DataModule { @Provides @Singleton fun providesConversationController( - historyController: HistoryController, + historyController: ChatHistoryController, chatServiceV2: ChatServiceV2, exchange: Exchange, conversationMapper: ConversationMapper, diff --git a/app/src/main/java/com/getcode/manager/AuthManager.kt b/app/src/main/java/com/getcode/manager/AuthManager.kt index 65ac31228..bdbec8c76 100644 --- a/app/src/main/java/com/getcode/manager/AuthManager.kt +++ b/app/src/main/java/com/getcode/manager/AuthManager.kt @@ -1,7 +1,6 @@ package com.getcode.manager import android.annotation.SuppressLint -import android.app.Activity import android.content.Context import com.bugsnag.android.Bugsnag import com.getcode.BuildConfig @@ -13,7 +12,7 @@ import com.getcode.model.AirdropType import com.getcode.model.PrefsBool import com.getcode.model.PrefsString import com.getcode.network.BalanceController -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.exchange.Exchange import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.IdentityRepository @@ -56,7 +55,7 @@ class AuthManager @Inject constructor( private val betaFlags: BetaFlagsRepository, private val exchange: Exchange, private val balanceController: BalanceController, - private val historyController: HistoryController, + private val historyController: ChatHistoryController, private val inMemoryDao: InMemoryDao, private val analytics: AnalyticsService, private val mnemonicManager: MnemonicManager, diff --git a/app/src/main/java/com/getcode/models/BillState.kt b/app/src/main/java/com/getcode/models/BillState.kt index d3ca32229..9fe31ab82 100644 --- a/app/src/main/java/com/getcode/models/BillState.kt +++ b/app/src/main/java/com/getcode/models/BillState.kt @@ -47,7 +47,7 @@ data class BillState( sealed interface Action { - val label: String + val label: String? @Composable get val asset: Painter @Composable get @@ -63,15 +63,15 @@ data class BillState( data class Share(override val action: () -> Unit): Action { override val label: String - @Composable get() = stringResource(R.string.action_share) + @Composable get() = stringResource(R.string.action_shareAsURL) override val asset: Painter @Composable get() = painterResource(id = R.drawable.ic_remote_send) } data class Cancel(override val action: () -> Unit): Action { - override val label: String - @Composable get() = stringResource(R.string.action_cancel) + override val label: String? + @Composable get() = null override val asset: Painter @Composable get() = painterResource(R.drawable.ic_bill_close) diff --git a/app/src/main/java/com/getcode/models/PaymentRequest.kt b/app/src/main/java/com/getcode/models/PaymentRequest.kt index 3735c4d91..3f1fcb00d 100644 --- a/app/src/main/java/com/getcode/models/PaymentRequest.kt +++ b/app/src/main/java/com/getcode/models/PaymentRequest.kt @@ -35,6 +35,19 @@ data class DeepLinkRequest( } companion object { + fun fromTipCardUsername(platform: String, username: String): DeepLinkRequest { + return DeepLinkRequest( + mode = Mode.Tip, + clientSecret = emptyList(), + tipRequest = TipRequest( + platformName = platform, + username = username + ), + cancelUrl = null, + successUrl = null, + ) + } + fun from(data: ByteArray?): DeepLinkRequest? { data ?: return null val container = runCatching { diff --git a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt index e275e4be9..2f7959bb9 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt @@ -4,57 +4,40 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.BubbleChart import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.zIndex import androidx.paging.compose.collectAsLazyPagingItems -import cafe.adriel.voyager.core.lifecycle.LifecycleEffect import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.hilt.getViewModel -import coil3.compose.AsyncImage -import coil3.compose.LocalPlatformContext -import coil3.request.ImageRequest -import coil3.request.error import com.getcode.R import com.getcode.model.ID import com.getcode.model.chat.Reference import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.theme.CodeTheme -import com.getcode.ui.components.SheetTitleDefaults -import com.getcode.ui.components.SheetTitleText -import com.getcode.ui.components.chat.AnonymousAvatar import com.getcode.ui.components.chat.UserAvatar import com.getcode.ui.components.chat.utils.localized -import com.getcode.ui.utils.getActivityScopedViewModel import com.getcode.util.formatDateRelatively -import com.getcode.view.main.balance.BalanceScreen -import com.getcode.view.main.balance.BalanceSheetViewModel import com.getcode.view.main.chat.ChatScreen import com.getcode.view.main.chat.ChatViewModel import com.getcode.view.main.chat.conversation.ChatConversationScreen import com.getcode.view.main.chat.conversation.ConversationViewModel -import com.getcode.view.main.home.HomeViewModel +import com.getcode.view.main.chat.list.ChatListScreen +import com.getcode.view.main.chat.list.ChatListViewModel import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -63,92 +46,23 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize -data object BalanceModal : ChatGraph, ModalRoot { +data object ChatListModal: ChatGraph, ModalRoot { + @IgnoredOnParcel override val key: ScreenKey = uniqueScreenKey - override val name: String - @Composable get() = stringResource(id = R.string.title_balance) + @Composable get() = stringResource(id = R.string.title_chat) @Composable override fun Content() { - val navigator = LocalCodeNavigator.current - - val viewModel = getActivityScopedViewModel() - val state by viewModel.stateFlow.collectAsState() - val isViewingBuckets by remember(state.isBucketDebuggerVisible) { - derivedStateOf { state.isBucketDebuggerVisible } - } - - val backButton = @Composable { - when { - isViewingBuckets -> SheetTitleDefaults.BackButton() - !isViewingBuckets && state.isBucketDebuggerEnabled -> { - Icon( - imageVector = Icons.Rounded.BubbleChart, - contentDescription = "", - tint = Color.White, - ) - } - - else -> Unit - } - } - ModalContainer( - navigator = navigator, - onLogoClicked = {}, - backButton = backButton, - backButtonEnabled = { isViewingBuckets || state.isBucketDebuggerEnabled }, - onBackClicked = when { - isViewingBuckets -> { - { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) - ) - } - } - - state.isBucketDebuggerEnabled -> { - { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(true) - ) - } - } - - else -> null - }, - closeButtonEnabled = close@{ - if (viewModel.stateFlow.value.isBucketDebuggerVisible) return@close false - if (navigator.isVisible) { - it is BalanceModal - } else { - navigator.progress > 0f - } - }, - onCloseClicked = null, + closeButtonEnabled = { it is ChatListModal }, ) { - BalanceScreen(state = state, dispatch = viewModel::dispatchEvent) + val viewModel = getViewModel() +// val conversations = viewModel.conversations.collectAsLazyPagingItems() + ChatListScreen(dispatch = {}) } - - LifecycleEffect( - onStarted = { - val disposedScreen = navigator.lastItem - if (disposedScreen !is BalanceModal) { - viewModel.dispatchEvent(BalanceSheetViewModel.Event.OnOpened) - } - }, - onDisposed = { - val disposedScreen = navigator.lastItem - if (disposedScreen !is BalanceModal) { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) - ) - } - } - ) } } @@ -198,7 +112,6 @@ data class ChatMessageConversationScreen( @Composable override fun Content() { val navigator = LocalCodeNavigator.current - val homeViewModel = getViewModel() val vm = getViewModel() val state by vm.stateFlow.collectAsState() diff --git a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt index 2d65674fa..e3e5ab27d 100644 --- a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt @@ -1,24 +1,38 @@ package com.getcode.navigation.screens +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BubbleChart import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.lifecycle.Lifecycle +import cafe.adriel.voyager.core.lifecycle.LifecycleEffect import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey import cafe.adriel.voyager.hilt.getViewModel import com.getcode.R import com.getcode.model.KinAmount +import com.getcode.models.DeepLinkRequest import com.getcode.navigation.core.LocalCodeNavigator +import com.getcode.ui.components.SheetTitleDefaults import com.getcode.ui.utils.RepeatOnLifecycle import com.getcode.ui.utils.getActivityScopedViewModel import com.getcode.utils.trace import com.getcode.view.download.ShareDownloadScreen import com.getcode.view.main.account.AccountHome import com.getcode.view.main.account.AccountSheetViewModel +import com.getcode.view.main.balance.BalanceScreen +import com.getcode.view.main.balance.BalanceSheetViewModel import com.getcode.view.main.giveKin.GiveKinScreen import com.getcode.view.main.home.HomeScreen import com.getcode.view.main.home.HomeViewModel import com.getcode.view.main.requestKin.RequestKinScreen +import com.google.firebase.encoders.annotations.Encodable.Ignore import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull @@ -31,13 +45,15 @@ sealed interface HomeResult { data class Request(val amount: KinAmount) : HomeResult data class ConfirmTip(val amount: KinAmount) : HomeResult data object ShowTipCard : HomeResult + data object CancelTipEntry: HomeResult } @Parcelize data class HomeScreen( val seed: String? = null, val cashLink: String? = null, - val requestPayload: String? = null, + @IgnoredOnParcel + val request: DeepLinkRequest? = null, ) : AppScreen(), MainGraph { @IgnoredOnParcel override val key: ScreenKey = uniqueScreenKey @@ -47,7 +63,7 @@ data class HomeScreen( val vm = getViewModel() trace("home rendered") - HomeScreen(vm, cashLink, requestPayload) + HomeScreen(vm, cashLink, request) OnScreenResult { result -> when (result) { @@ -66,6 +82,10 @@ data class HomeScreen( is HomeResult.ShowTipCard -> { vm.presentShareableTipCard() } + + is HomeResult.CancelTipEntry -> { + vm.cancelTipEntry() + } } } } @@ -78,7 +98,7 @@ data object GiveKinModal : AppScreen(), MainGraph, ModalRoot { override val name: String - @Composable get() = stringResource(id = R.string.title_giveKin) + @Composable get() = stringResource(id = R.string.title_giveCash) @Composable override fun Content() { @@ -184,6 +204,96 @@ data object ShareDownloadLinkModal : MainGraph, ModalRoot { } } +@Parcelize +data object BalanceModal : ChatGraph, ModalRoot { + @IgnoredOnParcel + override val key: ScreenKey = uniqueScreenKey + + + override val name: String + @Composable get() = stringResource(id = R.string.title_balance) + + @Composable + override fun Content() { + val navigator = LocalCodeNavigator.current + + val viewModel = getActivityScopedViewModel() + val state by viewModel.stateFlow.collectAsState() + val isViewingBuckets by remember(state.isBucketDebuggerVisible) { + derivedStateOf { state.isBucketDebuggerVisible } + } + + val backButton = @Composable { + when { + isViewingBuckets -> SheetTitleDefaults.BackButton() + !isViewingBuckets && state.isBucketDebuggerEnabled -> { + Icon( + imageVector = Icons.Rounded.BubbleChart, + contentDescription = "", + tint = Color.White, + ) + } + + else -> Unit + } + } + + ModalContainer( + navigator = navigator, + onLogoClicked = {}, + backButton = backButton, + backButtonEnabled = { isViewingBuckets || state.isBucketDebuggerEnabled }, + onBackClicked = when { + isViewingBuckets -> { + { + viewModel.dispatchEvent( + BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) + ) + } + } + + state.isBucketDebuggerEnabled -> { + { + viewModel.dispatchEvent( + BalanceSheetViewModel.Event.OnDebugBucketsVisible(true) + ) + } + } + + else -> null + }, + closeButtonEnabled = close@{ + if (viewModel.stateFlow.value.isBucketDebuggerVisible) return@close false + if (navigator.isVisible) { + it is BalanceModal + } else { + navigator.progress > 0f + } + }, + onCloseClicked = null, + ) { + BalanceScreen(state = state, dispatch = viewModel::dispatchEvent) + } + + LifecycleEffect( + onStarted = { + val disposedScreen = navigator.lastItem + if (disposedScreen !is BalanceModal) { + viewModel.dispatchEvent(BalanceSheetViewModel.Event.OnOpened) + } + }, + onDisposed = { + val disposedScreen = navigator.lastItem + if (disposedScreen !is BalanceModal) { + viewModel.dispatchEvent( + BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) + ) + } + } + ) + } +} + @Composable fun AppScreen.OnScreenResult(block: (T) -> Unit) { diff --git a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt index ad4a11eb4..51997739c 100644 --- a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt +++ b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt @@ -1,6 +1,7 @@ package com.getcode.navigation.screens import android.webkit.JavascriptInterface +import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding @@ -317,7 +318,7 @@ data class BuyMoreKinModal( override val key: ScreenKey = uniqueScreenKey override val name: String - @Composable get() = stringResource(id = R.string.action_buyMoreKin) + @Composable get() = stringResource(id = R.string.action_addCash) @Composable override fun Content() { @@ -366,7 +367,7 @@ data class KadoWebScreen(val url: String) : MainGraph, ModalContent { override val key: ScreenKey = uniqueScreenKey override val name: String - @Composable get() = stringResource(id = R.string.action_buyMoreKin) + @Composable get() = stringResource(id = R.string.action_addCash) @Composable override fun Content() { @@ -436,6 +437,9 @@ data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot { } else { navigator.progress > 0f } + }, + onCloseClicked = { + navigator.hideWithResult(HomeResult.CancelTipEntry) } ) { EnterTipScreen(getViewModel()) { result -> @@ -443,6 +447,10 @@ data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot { } } } + + BackHandler { + navigator.hideWithResult(HomeResult.CancelTipEntry) + } } } @@ -458,19 +466,35 @@ data class ConnectAccount( override fun Content() { val navigator = LocalCodeNavigator.current val viewModel = getViewModel() - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is ConnectAccount - } else { - navigator.progress > 0f + when (reason) { + IdentityConnectionReason.TipCard -> { + ModalContainer( + closeButtonEnabled = { + if (navigator.isVisible) { + it is ConnectAccount + } else { + navigator.progress > 0f + } + } + ) { + ConnectAccountScreen(viewModel) + } + } + IdentityConnectionReason.IdentityReveal -> { + ModalContainer( + backButtonEnabled = { + if (navigator.isVisible) { + it is ConnectAccount + } else { + navigator.progress > 0f + } + } + ) { + ConnectAccountScreen(viewModel) } } - ) { - ConnectAccountScreen(viewModel) } - LaunchedEffect(viewModel, reason) { viewModel.dispatchEvent(TipConnectViewModel.Event.OnReasonChanged(reason)) } diff --git a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt index 992378e30..f6c9c7303 100644 --- a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt +++ b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt @@ -3,7 +3,9 @@ package com.getcode.notifications import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.media.RingtoneManager import android.os.Build import androidx.core.app.NotificationCompat @@ -15,7 +17,7 @@ import com.getcode.manager.SessionManager import com.getcode.model.notifications.NotificationType import com.getcode.model.notifications.parse import com.getcode.network.BalanceController -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.TipController import com.getcode.network.repository.AccountRepository import com.getcode.network.repository.PushRepository @@ -25,7 +27,10 @@ import com.getcode.util.CurrencyUtils import com.getcode.util.resources.ResourceHelper import com.getcode.util.resources.ResourceType import com.getcode.utils.ErrorUtils +import com.getcode.utils.TraceType import com.getcode.utils.installationId +import com.getcode.utils.trace +import com.getcode.view.MainActivity import com.google.firebase.Firebase import com.google.firebase.installations.installations import com.google.firebase.messaging.FirebaseMessagingService @@ -68,7 +73,7 @@ class CodePushMessagingService : FirebaseMessagingService(), lateinit var balanceController: BalanceController @Inject - lateinit var historyController: HistoryController + lateinit var historyController: ChatHistoryController @Inject lateinit var tipController: TipController @@ -183,8 +188,18 @@ class CodePushMessagingService : FirebaseMessagingService(), .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setSmallIcon(R.drawable.ic_code_logo_outline) .setAutoCancel(true) + .setContentIntent(buildContentIntent(type)) notificationManager.notify(title.hashCode(), notificationBuilder.build()) + + trace( + tag = "Push", + message = "Push notification shown", + metadata = { + "category" to type.name + }, + type = TraceType.Process + ) } private fun updateOrganizerAndSwap() = launch { @@ -207,6 +222,19 @@ class CodePushMessagingService : FirebaseMessagingService(), } } +private fun Context.buildContentIntent(type: NotificationType): PendingIntent { + val launchIntent = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + return PendingIntent.getActivity( + this, + type.ordinal, + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) +} + private fun NotificationManager.getActiveNotification(notificationId: Int): Notification? { val barNotifications = getActiveNotifications() diff --git a/app/src/main/java/com/getcode/ui/components/AuthCheck.kt b/app/src/main/java/com/getcode/ui/components/AuthCheck.kt index 4b3821814..6734bbe67 100644 --- a/app/src/main/java/com/getcode/ui/components/AuthCheck.kt +++ b/app/src/main/java/com/getcode/ui/components/AuthCheck.kt @@ -85,6 +85,7 @@ fun AuthCheck( when (result.type) { is DeeplinkHandler.Type.Login -> true is DeeplinkHandler.Type.Cash, + is DeeplinkHandler.Type.Tip, is DeeplinkHandler.Type.Sdk -> { val hasAuth = state.isAuthenticated == true if (!hasAuth) { diff --git a/app/src/main/java/com/getcode/ui/components/CodeButton.kt b/app/src/main/java/com/getcode/ui/components/CodeButton.kt index 6bc5c515a..ea1c93525 100644 --- a/app/src/main/java/com/getcode/ui/components/CodeButton.kt +++ b/app/src/main/java/com/getcode/ui/components/CodeButton.kt @@ -38,9 +38,8 @@ enum class ButtonState { @Composable fun CodeButton( - modifier: Modifier = Modifier, - onClick: () -> Unit, text: String, + modifier: Modifier = Modifier, isLoading: Boolean = false, isSuccess: Boolean = false, enabled: Boolean = true, @@ -51,6 +50,7 @@ fun CodeButton( buttonState: ButtonState = ButtonState.Bordered, textColor: Color = Color.Unspecified, shape: Shape = CodeTheme.shapes.small, + onClick: () -> Unit, ) { CodeButton( modifier = modifier, diff --git a/app/src/main/java/com/getcode/ui/components/Pill.kt b/app/src/main/java/com/getcode/ui/components/Pill.kt index 897cebb44..7842cf6c8 100644 --- a/app/src/main/java/com/getcode/ui/components/Pill.kt +++ b/app/src/main/java/com/getcode/ui/components/Pill.kt @@ -51,7 +51,7 @@ fun Pill( fun Pill( modifier: Modifier = Modifier, backgroundColor: Color = Black50, - contentColor: Color = Color.White, + contentColor: Color = CodeTheme.colors.onAction, shape: CornerBasedShape = CircleShape, contentPadding: PaddingValues = PaddingValues( horizontal = CodeTheme.dimens.grid.x2, diff --git a/app/src/main/java/com/getcode/ui/components/SlideToConfirm.kt b/app/src/main/java/com/getcode/ui/components/SlideToConfirm.kt index 5ee5e40a7..c9f048093 100644 --- a/app/src/main/java/com/getcode/ui/components/SlideToConfirm.kt +++ b/app/src/main/java/com/getcode/ui/components/SlideToConfirm.kt @@ -3,6 +3,7 @@ package com.getcode.ui.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -178,7 +179,7 @@ fun SlideToConfirm( val hapticFeedback = LocalHapticFeedback.current val swipeState = rememberSwipeableState( - initialValue = if (loading) Anchor.End else Anchor.Start, + initialValue = Anchor.Start, ) val composeScope = rememberCoroutineScope() @@ -217,7 +218,6 @@ fun SlideToConfirm( hint(swipeFraction, PaddingValues(horizontal = Thumb.Size + CodeTheme.dimens.grid.x2), label) } - if (isSuccess) { Image( painter = painterResource(id = R.drawable.ic_check), @@ -227,9 +227,13 @@ fun SlideToConfirm( .align(Alignment.Center), ) } else { + val loadingColor by animateColorAsState( + targetValue = if (loading) Color.White else Color.Transparent + ) + CodeCircularProgressIndicator( strokeWidth = CodeTheme.dimens.thickBorder, - color = calculateLoadingColor(swipeFraction), + color = loadingColor, modifier = Modifier .size(CodeTheme.dimens.grid.x4) .align(Alignment.Center), @@ -362,10 +366,6 @@ private fun calculateHintTextColor(swipeFraction: Float): Color { return lerp(Color.White, Color.White.copy(alpha = 0f), fraction) } -private fun calculateLoadingColor(swipeFraction: Float): Color { - if (swipeFraction < 0.1f) return Color.White.copy(0f) - return Color.White -} @Preview @Composable diff --git a/app/src/main/java/com/getcode/ui/components/chat/ChatNode.kt b/app/src/main/java/com/getcode/ui/components/chat/ChatNode.kt index ee9f79a62..19e7de5ad 100644 --- a/app/src/main/java/com/getcode/ui/components/chat/ChatNode.kt +++ b/app/src/main/java/com/getcode/ui/components/chat/ChatNode.kt @@ -20,14 +20,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import coil3.compose.AsyncImage -import coil3.compose.LocalPlatformContext -import coil3.request.ImageRequest -import coil3.request.error import com.getcode.LocalBetaFlags -import com.getcode.R +import com.getcode.model.Conversation import com.getcode.model.chat.Chat import com.getcode.model.chat.MessageContent import com.getcode.theme.BrandLight @@ -38,7 +32,6 @@ import com.getcode.ui.components.chat.utils.localizedText import com.getcode.ui.utils.rememberedClickable import com.getcode.util.DateUtils import com.getcode.util.formatTimeRelatively -import java.util.UUID object ChatNodeDefaults { val UnreadIndicator: Color = Color(0xFF31BB00) @@ -62,7 +55,7 @@ fun ChatNode( horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), verticalAlignment = Alignment.CenterVertically ) { - if (betaFlags.tipsChatEnabled) { + if (betaFlags.conversationsEnabled) { val imageModifier = Modifier .size(CodeTheme.dimens.staticGrid.x10) .clip(CircleShape) diff --git a/app/src/main/java/com/getcode/ui/components/chat/TipChatActions.kt b/app/src/main/java/com/getcode/ui/components/chat/TipChatActions.kt index d79b196ef..6a1412f07 100644 --- a/app/src/main/java/com/getcode/ui/components/chat/TipChatActions.kt +++ b/app/src/main/java/com/getcode/ui/components/chat/TipChatActions.kt @@ -3,10 +3,6 @@ package com.getcode.ui.components.chat import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -25,7 +21,7 @@ internal fun TipChatActions( showTipActions: Boolean, openMessageChat: () -> Unit ) { - val tipChatsEnabled = LocalBetaFlags.current.tipsChatEnabled + val tipChatsEnabled = LocalBetaFlags.current.conversationsEnabled if (showTipActions) { if (tipChatsEnabled && contents.verb is Verb.ReceivedTip) { diff --git a/app/src/main/java/com/getcode/ui/utils/Modifier.kt b/app/src/main/java/com/getcode/ui/utils/Modifier.kt index c453337df..07e5dd865 100644 --- a/app/src/main/java/com/getcode/ui/utils/Modifier.kt +++ b/app/src/main/java/com/getcode/ui/utils/Modifier.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,6 +29,7 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onPlaced @@ -37,6 +40,7 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import com.getcode.theme.BrandLight +import com.getcode.theme.CodeTheme inline fun Modifier.addIf( predicate: Boolean, @@ -235,4 +239,130 @@ fun Modifier.drawWithGradient( blendMode = blendMode ) } +} + +private val gradientSize + @Composable get() = CodeTheme.dimens.staticGrid.x8 + +fun Modifier.verticalScrollStateGradient( + scrollState: LazyListState, + color: Color = Color.Unspecified, + showAtStart: Boolean = true, + showAtEnd: Boolean = true, + isLongGradient: Boolean = false, +): Modifier = composed { + val backgroundColor = color.takeOrElse { CodeTheme.colors.background } + val gradientSizePx = + with(LocalDensity.current) { gradientSize.toPx() } * if (isLongGradient) 1.5f else 1f + this + .addIf(showAtStart && !scrollState.isScrolledToStart()) { + Modifier.drawWithGradient( + color = backgroundColor, + startY = { gradientSizePx }, + endY = { 0f }, + ) + } + .addIf(showAtEnd && !scrollState.isScrolledToEnd()) { + Modifier.drawWithGradient( + color = backgroundColor, + startY = { size.height - gradientSizePx }, + ) + } +} + +fun Modifier.horizontalScrollStateGradient( + scrollState: LazyListState, + color: Color, + showAtStart: Boolean = true, + showAtEnd: Boolean = true, +): Modifier = composed { + val gradientSizePx = + with(LocalDensity.current) { gradientSize.toPx() } + this + .addIf(showAtStart && !scrollState.isScrolledToStart()) { + Modifier.drawWithContent { + drawContent() + drawRect( + brush = Brush.horizontalGradient( + colors = listOf( + color, + Color.Transparent, + ), + startX = 0f, + endX = gradientSizePx, + ), + ) + } + } + .addIf(showAtEnd && !scrollState.isScrolledToEnd()) { + Modifier.drawWithContent { + drawContent() + drawRect( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + color, + ), + startX = size.width - gradientSizePx, + endX = size.width, + ), + ) + } + } +} + +fun Modifier.verticalScrollStateGradient( + scrollState: LazyGridState, + color: Color, + showAtStart: Boolean = true, + showAtEnd: Boolean = true, +): Modifier = composed { + val gradientSizePx = + with(LocalDensity.current) { gradientSize.toPx() } + this + .addIf(showAtStart && !scrollState.isVerticallyScrolledToStart()) { + Modifier.drawWithGradient( + color = color, + startY = { gradientSizePx }, + endY = { 0f }, + ) + } + .addIf(showAtEnd && !scrollState.isVerticallyScrolledToEnd()) { + Modifier.drawWithGradient( + color = color, + startY = { size.height - gradientSizePx }, + ) + } +} + +fun LazyListState.isScrolledToEnd(): Boolean { + val lastItem = layoutInfo.visibleItemsInfo.lastOrNull() + return lastItem == null || + (lastItem.index == layoutInfo.totalItemsCount - 1 && lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset) +} + +fun LazyListState.isScrolledToStart(): Boolean { + val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() + return firstItem == null || firstItem.offset == 0 +} + +fun LazyGridState.isVerticallyScrolledToEnd(): Boolean { + val lastItem = layoutInfo.visibleItemsInfo.lastOrNull() + return lastItem == null || + (lastItem.index == layoutInfo.totalItemsCount - 1 && lastItem.size.height + lastItem.offset.y <= layoutInfo.viewportEndOffset) +} + +fun LazyGridState.isVerticallyScrolledToStart(): Boolean { + val firstItem = layoutInfo.visibleItemsInfo.firstOrNull() + return firstItem == null || firstItem.offset.y == 0 +} + +fun Modifier.footerShadow() = this.drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient( + endY = size.height * 0.12f, + colors = listOf(Color(0x10000000), Color.Transparent), + ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/getcode/util/AccountUtils.kt b/app/src/main/java/com/getcode/util/AccountUtils.kt index d3621c846..0d1c2b1d9 100644 --- a/app/src/main/java/com/getcode/util/AccountUtils.kt +++ b/app/src/main/java/com/getcode/util/AccountUtils.kt @@ -9,6 +9,7 @@ import android.os.HandlerThread import androidx.core.os.bundleOf import com.getcode.BuildConfig import com.getcode.utils.TraceType +import com.getcode.utils.network.retryable import com.getcode.utils.trace import io.reactivex.rxjava3.annotations.NonNull import io.reactivex.rxjava3.core.Single @@ -51,7 +52,19 @@ object AccountUtils { val subject = SingleSubject.create>() return subject.doOnSubscribe { CoroutineScope(Dispatchers.IO).launch { - val result = getAccountNoActivity(context) + val result = retryable( + call = { getAccountNoActivity(context) }, + onRetry = { currentAttempt -> + trace( + tag = "Account", + message = "Retrying call", + metadata = { + "count" to currentAttempt + }, + type = TraceType.Process, + ) + } + ) subject.onSuccess(result ?: (null to null)) } } @@ -71,10 +84,10 @@ object AccountUtils { ): Pair? = suspendCancellableCoroutine { cont -> trace("getAuthToken", type = TraceType.Silent) val am: AccountManager = AccountManager.get(context) - val account = am.accounts.getOrNull(0) + val account = am.getAccountsByType(ACCOUNT_TYPE).firstOrNull() if (account == null) { trace("no associated account found", type = TraceType.Error) - cont.resume(null to null) + cont.resume(null) return@suspendCancellableCoroutine } val start = Clock.System.now() @@ -90,9 +103,8 @@ object AccountUtils { cont.resume(authToken.orEmpty() to account) } catch (e: AuthenticatorException) { - e.printStackTrace() trace(message = "failed to read account", error = e, type = TraceType.Error) - cont.resume(null to null) + cont.resume(null) } }, handler ) @@ -110,6 +122,19 @@ object AccountUtils { } } - return getAccountNoActivity(context)?.first + return retryable( + call = { getAccountNoActivity(context) }, + onRetry = { currentAttempt -> + trace( + tag = "Account", + message = "Retrying call", + metadata = { + "count" to currentAttempt + }, + type = TraceType.Process, + ) + } + + )?.first } } \ No newline at end of file diff --git a/app/src/main/java/com/getcode/util/AuthenticatorService.kt b/app/src/main/java/com/getcode/util/AuthenticatorService.kt index fc45de3b1..902394748 100644 --- a/app/src/main/java/com/getcode/util/AuthenticatorService.kt +++ b/app/src/main/java/com/getcode/util/AuthenticatorService.kt @@ -10,8 +10,9 @@ import javax.inject.Inject @AndroidEntryPoint class AuthenticatorService : Service() { - @Inject - lateinit var accountAuthenticator: AccountAuthenticator + private val accountAuthenticator: AccountAuthenticator by lazy { + AccountAuthenticator(this) + } override fun onBind(intent: Intent): IBinder? { var binder: IBinder? = null diff --git a/app/src/main/java/com/getcode/util/Context.kt b/app/src/main/java/com/getcode/util/Context.kt index 1060a721a..867d5a51f 100644 --- a/app/src/main/java/com/getcode/util/Context.kt +++ b/app/src/main/java/com/getcode/util/Context.kt @@ -1,15 +1,8 @@ package com.getcode.util import android.content.Context -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.biometric.BiometricPrompt.AuthenticationError import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import com.getcode.R -import com.getcode.network.repository.TransactionRepository.ErrorSubmitIntent -import timber.log.Timber -import java.util.concurrent.Executors fun Context.launchAppSettings() { val intent = IntentUtils.appSettings() diff --git a/app/src/main/java/com/getcode/util/DateUtils.kt b/app/src/main/java/com/getcode/util/DateUtils.kt index 91fa913d4..bcce855dd 100644 --- a/app/src/main/java/com/getcode/util/DateUtils.kt +++ b/app/src/main/java/com/getcode/util/DateUtils.kt @@ -5,6 +5,8 @@ import android.text.format.DateFormat import android.text.format.DateUtils import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import com.getcode.utils.atStartOfDay +import com.getcode.utils.toLocalDate import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.Calendar diff --git a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt index 813209b9e..b9c2e44a5 100644 --- a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt +++ b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt @@ -1,13 +1,20 @@ package com.getcode.util +import android.content.Context import android.content.Intent import android.net.Uri +import androidx.core.net.toUri import cafe.adriel.voyager.core.screen.Screen +import com.getcode.model.PrefsBool +import com.getcode.models.DeepLinkRequest import com.getcode.navigation.screens.HomeScreen import com.getcode.navigation.screens.LoginScreen +import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.urlDecode import com.getcode.utils.TraceType +import com.getcode.utils.base64EncodedData import com.getcode.utils.trace +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber import javax.inject.Inject @@ -17,6 +24,7 @@ data class DeeplinkResult( val type: DeeplinkHandler.Type, val stack: List, ) + /** * This class is used to manage intent state across navigation. * @@ -31,7 +39,10 @@ data class DeeplinkResult( * in favour of the latest request in the navigation graph. */ @Singleton -class DeeplinkHandler @Inject constructor() { +class DeeplinkHandler @Inject constructor( + @ApplicationContext private val context: Context, + private val betaFlags: BetaFlagsRepository +) { var debounceIntent: Intent? = null set(value) { intent.value = value @@ -41,8 +52,18 @@ class DeeplinkHandler @Inject constructor() { val intent = MutableStateFlow(debounceIntent) - fun handle(intent: Intent? = debounceIntent): DeeplinkResult? { - val uri = intent?.data ?: return null + suspend fun handle(intent: Intent? = debounceIntent): DeeplinkResult? { + println(intent) + val uri = when { + intent?.data != null -> intent.data + intent?.getStringExtra(Intent.EXTRA_TEXT) != null -> { + val sharedLink = intent.getStringExtra(Intent.EXTRA_TEXT)?.toUri() ?: return null + sharedLink.resolveSharedEntity() + } + + else -> null + } ?: return null + return when (val type = uri.deeplinkType) { is Type.Login -> { DeeplinkResult( @@ -61,9 +82,25 @@ class DeeplinkHandler @Inject constructor() { is Type.Sdk -> { Timber.d("sdk=${type.payload}") + val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) } DeeplinkResult( type, - listOf(HomeScreen(requestPayload = type.payload)), + listOf(HomeScreen(request = request)), + ) + } + + is Type.Tip -> { + Timber.d("tipcard for ${type.username} on ${type.platform}") + DeeplinkResult( + type, + listOf( + HomeScreen( + request = DeepLinkRequest.fromTipCardUsername( + type.platform, + type.username + ) + ) + ), ) } @@ -71,21 +108,48 @@ class DeeplinkHandler @Inject constructor() { } } - private val Uri.deeplinkType: Type - get() = when (val segment = lastPathSegment) { - "login" -> { - var entropy = fragments[Key.entropy] - if (entropy == null) { - entropy = this.getQueryParameter("data") + /** + * Handles converting inbound shared content with possible deeplinks + * e.g sharing a tweet to trigger a tipcard flow + */ + private suspend fun Uri.resolveSharedEntity(): Uri { + when { + this.host == "x.com" || this.host == "twitter.com" -> { + // https://x.com//status/ + if (betaFlags.isEnabled(PrefsBool.SHARE_TWEET_TO_TIP)) { + // convert shared tweets to owner's tip card + val username = pathSegments.firstOrNull() ?: return this + return Uri.parse(Linkify.tipCard(username, "x")) } + } + } + return this + } - Type.Login(entropy.also { Timber.d("entropy=$it") }) + private val Uri.deeplinkType: Type + get() { + // check for tipcard URLs + val components = pathSegments + if (components.count() >= 2 && components[0] == "x" && components[1].isNotEmpty()) { + return Type.Tip(components[0], components[1]) } - "cash", "c" -> Type.Cash(fragments[Key.entropy]) - // support all variations of SDK request triggers - in Type.Sdk.regex -> Type.Sdk(fragments[Key.payload]?.urlDecode()) - else -> Type.Unknown(path = segment) + return when (val segment = lastPathSegment) { + "login" -> { + var entropy = fragments[Key.entropy] + if (entropy == null) { + entropy = this.getQueryParameter("data") + } + + Type.Login(entropy.also { Timber.d("entropy=$it") }) + } + + "cash", "c" -> Type.Cash(fragments[Key.entropy]) + + // support all variations of SDK request triggers + in Type.Sdk.regex -> Type.Sdk(fragments[Key.payload]?.urlDecode()) + else -> Type.Unknown(path = segment) + } } private val Uri.fragments: Map @@ -104,11 +168,13 @@ class DeeplinkHandler @Inject constructor() { sealed interface Type { data class Login(val link: String?) : Type data class Cash(val link: String?) : Type + data class Tip(val platform: String, val username: String) : Type data class Sdk(val payload: String?) : Type { companion object { val regex = Regex("^(login|payment|tip)?-?request-(modal|page)-(mobile|desktop)\$") } } + data class Unknown(val path: String?) : Type } @@ -140,4 +206,5 @@ class DeeplinkHandler @Inject constructor() { } } -private operator fun Regex.contains(text: String?): Boolean = text?.let { this.matches(it) } ?: false +private operator fun Regex.contains(text: String?): Boolean = + text?.let { this.matches(it) } ?: false diff --git a/app/src/main/java/com/getcode/util/IntentUtils.kt b/app/src/main/java/com/getcode/util/IntentUtils.kt index 835b22f91..ada046265 100644 --- a/app/src/main/java/com/getcode/util/IntentUtils.kt +++ b/app/src/main/java/com/getcode/util/IntentUtils.kt @@ -5,15 +5,9 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import com.getcode.BuildConfig -import com.getcode.R -import com.getcode.model.Currency -import com.getcode.model.KinAmount -import com.getcode.model.Username -import com.getcode.network.repository.replaceParam -import com.getcode.network.repository.urlEncode -import com.getcode.solana.organizer.GiftCardAccount import com.getcode.utils.makeE164 + object IntentUtils { fun appSettings() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { @@ -30,7 +24,7 @@ object IntentUtils { } fun tweet(message: String) = Intent(Intent.ACTION_VIEW).apply { - val url = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}" + val url = Linkify.tweet(message) setData(Uri.parse(url)) flags = Intent.FLAG_ACTIVITY_NEW_TASK } @@ -47,8 +41,8 @@ object IntentUtils { return shareIntent } - fun tipCard(username: String): Intent { - val url = "https://tipcard.getcode.com/x/$username" + fun tipCard(username: String, platform: String): Intent { + val url = Linkify.tipCard(username, platform) val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -67,7 +61,7 @@ object IntentUtils { entropy: String, formattedAmount: String, ): Intent { - val url = "https://cash.getcode.com/c/#/e=$entropy" + val url = Linkify.cashLink(entropy) val text = "$formattedAmount $url" val sendIntent: Intent = Intent().apply { diff --git a/app/src/main/java/com/getcode/util/Linkify.kt b/app/src/main/java/com/getcode/util/Linkify.kt new file mode 100644 index 000000000..3eb42501f --- /dev/null +++ b/app/src/main/java/com/getcode/util/Linkify.kt @@ -0,0 +1,9 @@ +package com.getcode.util + +import com.getcode.network.repository.urlEncode + +object Linkify { + fun cashLink(entropy: String): String = "https://cash.getcode.com/c/#/e=${entropy}" + fun tipCard(username: String, platform: String): String = "https://tipcard.getcode.com/${platform}/${username}" + fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}" +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/util/Pacman.kt b/app/src/main/java/com/getcode/util/Pacman.kt new file mode 100644 index 000000000..db2500529 --- /dev/null +++ b/app/src/main/java/com/getcode/util/Pacman.kt @@ -0,0 +1,30 @@ +package com.getcode.util + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + + +class Pacman @Inject constructor( + @ApplicationContext private val context: Context +) { + private val packageManager = context.packageManager + + fun enableTweetShare(enable: Boolean) { + val component = ComponentName(context.packageName, "com.getcode.view.TweetShareHandler") + if (enable) { + packageManager.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP + ) + } else { + packageManager.setComponentEnabledSetting( + component, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/MainActivity.kt b/app/src/main/java/com/getcode/view/MainActivity.kt index 68302a2c1..b8c76f03e 100644 --- a/app/src/main/java/com/getcode/view/MainActivity.kt +++ b/app/src/main/java/com/getcode/view/MainActivity.kt @@ -19,6 +19,7 @@ import com.getcode.LocalNetworkObserver import com.getcode.LocalPhoneFormatter import com.getcode.R import com.getcode.analytics.AnalyticsService +import com.getcode.network.TipController import com.getcode.network.client.Client import com.getcode.network.exchange.Exchange import com.getcode.ui.tips.DefinedTips @@ -64,6 +65,9 @@ class MainActivity : FragmentActivity() { @Inject lateinit var exchange: Exchange + @Inject + lateinit var tipController: TipController + @Inject lateinit var tipEngine: TipsEngine @@ -135,11 +139,13 @@ class MainActivity : FragmentActivity() { override fun onResume() { super.onResume() client.startTimer() + tipController.checkForConnection() } override fun onStop() { super.onStop() client.stopTimer() + tipController.stopTimer() } } diff --git a/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt b/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt index 62ebb0f17..3ff0831a3 100644 --- a/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt +++ b/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt @@ -14,7 +14,9 @@ import com.getcode.manager.SessionManager import com.getcode.manager.TopBarManager import com.getcode.media.MediaScanner import com.getcode.network.repository.TransactionRepository -import com.getcode.network.repository.TransactionRepository.DeniedReason +import com.getcode.network.repository.DeniedReason +import com.getcode.network.repository.ErrorSubmitIntent +import com.getcode.network.repository.ErrorSubmitIntentException import com.getcode.network.repository.decodeBase64 import com.getcode.theme.Alert import com.getcode.theme.Brand @@ -299,9 +301,9 @@ abstract class BaseAccessKeyViewModel( internal fun onSubmitError(e: Throwable) { when (e) { - is TransactionRepository.ErrorSubmitIntentException -> { + is ErrorSubmitIntentException -> { when (val intent = e.errorSubmitIntent) { - is TransactionRepository.ErrorSubmitIntent.Denied -> { + is ErrorSubmitIntent.Denied -> { if (intent.reasons.isEmpty() || intent.reasons.first() == DeniedReason.Unspecified) { getSomethingWentWrongError() } else { diff --git a/app/src/main/java/com/getcode/view/login/LoginHome.kt b/app/src/main/java/com/getcode/view/login/LoginHome.kt index 360665e9b..08933b03c 100644 --- a/app/src/main/java/com/getcode/view/login/LoginHome.kt +++ b/app/src/main/java/com/getcode/view/login/LoginHome.kt @@ -87,7 +87,7 @@ fun LoginHome( ) CodeButton( - Modifier + modifier = Modifier .fillMaxWidth() .constrainAs(buttonCreate) { top.linkTo(logo.bottom) //possibly remove!! @@ -99,7 +99,7 @@ fun LoginHome( buttonState = ButtonState.Filled, ) CodeButton( - Modifier + modifier = Modifier .fillMaxWidth() .constrainAs(buttonLogin) { top.linkTo(buttonCreate.bottom) diff --git a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt index e4784a550..70c69390d 100644 --- a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt +++ b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.Text @@ -47,11 +48,12 @@ import com.getcode.navigation.screens.BuySellScreen import com.getcode.navigation.screens.DepositKinScreen import com.getcode.navigation.screens.FaqScreen import com.getcode.navigation.screens.WithdrawalAmountScreen -import com.getcode.theme.BrandLight import com.getcode.theme.CodeTheme import com.getcode.theme.White10 +import com.getcode.ui.components.CodeScaffold import com.getcode.ui.utils.getActivity import com.getcode.ui.utils.rememberedClickable +import com.getcode.ui.utils.verticalScrollStateGradient import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -86,6 +88,7 @@ fun AccountHome( navigator.push(BuySellScreen) } } + AccountPage.DEPOSIT -> navigator.push(DepositKinScreen) AccountPage.WITHDRAW -> navigator.push(WithdrawalAmountScreen) AccountPage.FAQ -> navigator.push(FaqScreen) @@ -120,18 +123,11 @@ fun AccountHome( } } - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(dataState.items, key = { it.type }, contentType = { it }) { item -> - ListItem(item = item) { - handleItemClicked(item.type) - } - } - - item { + CodeScaffold( + bottomBar = { Box(modifier = Modifier.fillMaxWidth()) { Text( modifier = Modifier - .padding(top = CodeTheme.dimens.grid.x7) .fillMaxWidth() .align(Alignment.Center), text = "Version ${BuildConfig.VERSION_NAME} • Build ${BuildConfig.VERSION_CODE}", @@ -142,6 +138,24 @@ fun AccountHome( ) } } + ) { padding -> + val listState = rememberLazyListState() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScrollStateGradient( + scrollState = listState, + isLongGradient = true, + ), + state = listState, + ) { + items(dataState.items, key = { it.type }, contentType = { it }) { item -> + ListItem(item = item) { + handleItemClicked(item.type) + } + } + } } } diff --git a/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt index cd26e9eaf..fb4ad7653 100644 --- a/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt @@ -170,7 +170,7 @@ class AccountSheetViewModel @Inject constructor( when (it.type) { AccountPage.BUY_KIN -> { if (buyModuleEnabled) { - it.copy(name = R.string.action_buyMoreKin) + it.copy(name = R.string.action_addCash) } else { it.copy(name = R.string.title_buySellKin) } diff --git a/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt index 7cd07999b..2e52c383c 100644 --- a/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt +++ b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.getcode.R import com.getcode.model.PrefsBool +import com.getcode.network.repository.BetaOptions import com.getcode.theme.CodeTheme import com.getcode.ui.components.ButtonState import com.getcode.ui.components.CodeButton @@ -44,7 +45,7 @@ fun BetaFlagsScreen( PrefsBool.VIBRATE_ON_SCAN, R.string.beta_vibrate_on_scan, stringResource(R.string.beta_vibrate_on_scan_description), - state.isVibrateOnScan + state.tickOnScan ), BetaFeature( PrefsBool.SHOW_CONNECTIVITY_STATUS, @@ -62,7 +63,7 @@ fun BetaFlagsScreen( PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, R.string.beta_balance_currency, stringResource(R.string.beta_balance_currency_description), - state.currencySelectionBalanceEnabled + state.balanceCurrencySelectionEnabled ), BetaFeature( PrefsBool.GIVE_REQUESTS_ENABLED, @@ -74,7 +75,7 @@ fun BetaFlagsScreen( PrefsBool.BUY_MODULE_ENABLED, R.string.beta_buy_kin, stringResource(id = R.string.beta_buy_kin_description), - state.buyKinEnabled + state.buyModuleEnabled ), BetaFeature( PrefsBool.CHAT_UNSUB_ENABLED, @@ -89,16 +90,22 @@ fun BetaFlagsScreen( state.tipsEnabled, ), BetaFeature( - PrefsBool.TIPS_CHAT_ENABLED, - R.string.beta_tipchats, - stringResource(id = R.string.beta_tipchats_description), - state.tipsChatEnabled, + PrefsBool.TIP_CARD_ON_HOMESCREEN, + R.string.beta_tipcard_on_homescreen, + stringResource(id = R.string.beta_tipcard_on_homescreen_description), + state.tipCardOnHomeScreen, ), BetaFeature( - PrefsBool.TIPS_CHAT_CASH_ENABLED, - R.string.beta_tipchats_cash, - stringResource(id = R.string.beta_tipchats_cash_description), - state.tipsChatCashEnabled, + PrefsBool.CONVERSATIONS_ENABLED, + R.string.beta_conversations, + stringResource(id = R.string.beta_conversations_description), + state.conversationsEnabled, + ), + BetaFeature( + PrefsBool.CONVERSATION_CASH_ENABLED, + R.string.beta_conversations_cash, + stringResource(id = R.string.beta_conversations_cash_description), + state.conversationCashEnabled, ), BetaFeature( PrefsBool.KADO_WEBVIEW_ENABLED, @@ -106,6 +113,12 @@ fun BetaFlagsScreen( stringResource(id = R.string.beta_kado_webview_description), state.kadoWebViewEnabled, ), + BetaFeature( + PrefsBool.SHARE_TWEET_TO_TIP, + R.string.beta_share_tweet_tip, + stringResource(id = R.string.beta_share_tweet_tip_description), + state.shareTweetToTip, + ), BetaFeature( PrefsBool.DISPLAY_ERRORS, R.string.beta_display_errors, @@ -143,12 +156,12 @@ fun BetaFlagsScreen( } } -private fun BetaFlagsViewModel.State.canMutate(flag: PrefsBool): Boolean { +private fun BetaOptions.canMutate(flag: PrefsBool): Boolean { return when (flag) { PrefsBool.BUY_MODULE_ENABLED -> false PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED -> false PrefsBool.TIPS_ENABLED -> false - PrefsBool.TIPS_CHAT_CASH_ENABLED -> tipsChatEnabled + PrefsBool.CONVERSATION_CASH_ENABLED -> conversationsEnabled else -> true } } diff --git a/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt b/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt index d212ecaab..ad4aabecd 100644 --- a/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt @@ -5,6 +5,7 @@ import com.getcode.model.PrefsBool import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.BetaOptions import com.getcode.network.repository.PrefRepository +import com.getcode.util.Pacman import com.getcode.utils.ErrorUtils import com.getcode.view.BaseViewModel2 import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,26 +19,11 @@ import javax.inject.Inject class BetaFlagsViewModel @Inject constructor( betaFlags: BetaFlagsRepository, prefRepository: PrefRepository, -) : BaseViewModel2( - initialState = State(), + pacman: Pacman, +) : BaseViewModel2( + initialState = BetaOptions.Defaults, updateStateForEvent = updateStateForEvent ) { - data class State( - val showNetworkDropOff: Boolean = false, - val canViewBuckets: Boolean = false, - val isVibrateOnScan: Boolean = false, - val currencySelectionBalanceEnabled: Boolean = false, - val displayErrors: Boolean = false, - val giveRequestsEnabled: Boolean = false, - val buyKinEnabled: Boolean = false, - val establishCodeRelationship: Boolean = false, - val chatUnsubEnabled: Boolean = false, - val tipsEnabled: Boolean = false, - val tipsChatEnabled: Boolean = false, - val tipsChatCashEnabled: Boolean = false, - val kadoWebViewEnabled: Boolean = false, - ) - sealed interface Event { data class UpdateSettings(val settings: BetaOptions) : Event data class Toggle(val setting: PrefsBool, val state: Boolean): Event @@ -58,29 +44,24 @@ class BetaFlagsViewModel @Inject constructor( it.setting, it.state ) + + when (it.setting) { + PrefsBool.SHARE_TWEET_TO_TIP -> { + pacman.enableTweetShare(it.state) + } + PrefsBool.DISPLAY_ERRORS -> { + ErrorUtils.setDisplayErrors(it.state) + } + else -> Unit + } }.launchIn(viewModelScope) } companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> + val updateStateForEvent: (Event) -> ((BetaOptions) -> BetaOptions) = { event -> when (event) { - is Event.UpdateSettings -> { state -> - with(event.settings) { - state.copy( - showNetworkDropOff = showNetworkDropOff, - canViewBuckets = canViewBuckets, - isVibrateOnScan = tickOnScan, - currencySelectionBalanceEnabled = balanceCurrencySelectionEnabled, - displayErrors = displayErrors, - giveRequestsEnabled = giveRequestsEnabled, - buyKinEnabled = buyModuleEnabled, - chatUnsubEnabled = chatUnsubEnabled, - tipsEnabled = tipsEnabled, - tipsChatEnabled = tipsChatEnabled, - tipsChatCashEnabled = tipsChatCashEnabled, - kadoWebViewEnabled = kadoWebViewEnabled, - ) - } + is Event.UpdateSettings -> { _ -> + event.settings } is Event.Toggle -> { state -> state } diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt index 13ad7d0be..ba0f21248 100644 --- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt @@ -15,7 +15,7 @@ import com.getcode.model.Rate import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.screens.HomeScreen import com.getcode.navigation.screens.WithdrawalArgs -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.client.* import com.getcode.util.resources.ResourceHelper import com.getcode.utils.ErrorUtils @@ -44,7 +44,7 @@ data class AccountWithdrawSummaryUiModel( class AccountWithdrawSummaryViewModel @Inject constructor( private val analytics: AnalyticsService, private val client: Client, - private val historyController: HistoryController, + private val historyController: ChatHistoryController, private val resources: ResourceHelper, ) : BaseViewModel(resources) { val uiFlow = MutableStateFlow(AccountWithdrawSummaryUiModel()) diff --git a/app/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt b/app/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt index 4ee9e802b..e94c9a1fa 100644 --- a/app/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt +++ b/app/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt @@ -186,7 +186,7 @@ fun BalanceContent( ) } }, - text = stringResource(id = R.string.action_buyMoreKin) + text = stringResource(id = R.string.action_addCash) ) } } diff --git a/app/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt b/app/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt index b6afbdaa1..03f43098f 100644 --- a/app/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt @@ -9,7 +9,7 @@ import com.getcode.model.Feature import com.getcode.model.PrefsBool import com.getcode.model.Rate import com.getcode.network.BalanceController -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.repository.FeatureRepository import com.getcode.network.repository.PrefRepository import com.getcode.util.Kin @@ -18,6 +18,7 @@ import com.getcode.view.BaseViewModel2 import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn @@ -29,7 +30,7 @@ import javax.inject.Inject @HiltViewModel class BalanceSheetViewModel @Inject constructor( balanceController: BalanceController, - historyController: HistoryController, + historyController: ChatHistoryController, prefsRepository: PrefRepository, features: FeatureRepository, networkObserver: NetworkConnectivityListener, @@ -101,7 +102,7 @@ class BalanceSheetViewModel @Inject constructor( } .launchIn(viewModelScope) - historyController.chats + historyController.notifications .onEach { if (it == null || (it.isEmpty() && !networkObserver.isConnected)) { dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(true)) @@ -124,6 +125,7 @@ class BalanceSheetViewModel @Inject constructor( eventFlow .filterIsInstance() + .filter { features.isEnabled(PrefsBool.CONVERSATIONS_ENABLED) } .onEach { historyController.fetchChats(true) } .launchIn(viewModelScope) } diff --git a/app/src/main/java/com/getcode/view/main/chat/ChatScreen.kt b/app/src/main/java/com/getcode/view/main/chat/ChatScreen.kt index ce26aaeae..19c1a44e4 100644 --- a/app/src/main/java/com/getcode/view/main/chat/ChatScreen.kt +++ b/app/src/main/java/com/getcode/view/main/chat/ChatScreen.kt @@ -22,6 +22,7 @@ import com.getcode.ui.components.VerticalDivider import com.getcode.ui.components.chat.MessageList import com.getcode.ui.components.chat.MessageListEvent import com.getcode.ui.components.chat.utils.ChatItem +import com.getcode.ui.components.chat.utils.localized import com.getcode.ui.utils.withTopBorder @Composable @@ -33,7 +34,7 @@ fun ChatScreen( val listState = rememberLazyListState() val context = LocalContext.current - val title = state.title + val title = state.title.localized Column(modifier = Modifier.fillMaxSize()) { MessageList( diff --git a/app/src/main/java/com/getcode/view/main/chat/ChatViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/ChatViewModel.kt index 673101e2f..1630fbd94 100644 --- a/app/src/main/java/com/getcode/view/main/chat/ChatViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/chat/ChatViewModel.kt @@ -12,7 +12,7 @@ import com.getcode.model.chat.Reference import com.getcode.model.chat.Title import com.getcode.model.chat.Verb import com.getcode.network.ConversationController -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.repository.BetaFlagsRepository import com.getcode.network.repository.base58 import com.getcode.ui.components.chat.utils.ChatItem @@ -40,7 +40,7 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class ChatViewModel @Inject constructor( - historyController: HistoryController, + historyController: ChatHistoryController, conversationController: ConversationController, betaFlags: BetaFlagsRepository, ) : BaseViewModel2( @@ -87,7 +87,7 @@ class ChatViewModel @Inject constructor( .onEach { Timber.d("chatid=${it?.base58}") } .filterNotNull() .onEach { historyController.advanceReadPointer(it) } - .flatMapLatest { historyController.chats } + .flatMapLatest { historyController.notifications } .flowOn(Dispatchers.IO) .filterNotNull() .mapNotNull { chats -> chats.firstOrNull { it.id == stateFlow.value.chatId } } diff --git a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt index a7b67569e..f96c74a29 100644 --- a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt @@ -12,12 +12,11 @@ import androidx.paging.map import com.getcode.BuildConfig import com.getcode.R import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager import com.getcode.model.ConversationWithLastPointers import com.getcode.model.Feature import com.getcode.model.ID import com.getcode.model.MessageStatus -import com.getcode.model.TipChatCashFeature +import com.getcode.model.ConversationCashFeature import com.getcode.model.TwitterUser import com.getcode.model.chat.ChatType import com.getcode.model.chat.Platform @@ -35,16 +34,13 @@ import com.getcode.utils.timestamp import com.getcode.view.BaseViewModel2 import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -91,7 +87,7 @@ class ConversationViewModel @Inject constructor( val Default = State( conversationId = null, reference = null, - tipChatCash = TipChatCashFeature(), + tipChatCash = ConversationCashFeature(), title = "Anonymous Tipper", textFieldState = TextFieldState(), identityAvailable = false, @@ -186,7 +182,7 @@ class ConversationViewModel @Inject constructor( .onEach { dispatchEvent(Event.OnConversationChanged(it)) } .launchIn(viewModelScope) - features.tipChatCash + features.conversationsCash .onEach { dispatchEvent(Event.OnTipsChatCashChanged(it)) } .launchIn(viewModelScope) diff --git a/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt b/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt new file mode 100644 index 000000000..01c2210bb --- /dev/null +++ b/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt @@ -0,0 +1,39 @@ +package com.getcode.view.main.chat.list + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.getcode.theme.CodeTheme +import com.getcode.ui.components.ButtonState +import com.getcode.ui.components.CodeButton +import com.getcode.ui.components.CodeScaffold + +@Composable +fun ChatListScreen( + dispatch: (ChatListViewModel.Event) -> Unit, +) { + CodeScaffold( + bottomBar = { + Box(modifier = Modifier.fillMaxWidth()) { + CodeButton( + modifier = Modifier.fillMaxWidth().padding(horizontal = CodeTheme.dimens.inset), + buttonState = ButtonState.Filled, + text = "Start a New Chat" + ) { + + } + } + } + ) { padding -> + LazyColumn(modifier = Modifier.padding(padding)) { +// items(conversations.itemCount) { index -> +// conversations[index]?.let { chat -> +// ChatNode(chat = chat) { } +// } +// } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt new file mode 100644 index 000000000..7be3733e6 --- /dev/null +++ b/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt @@ -0,0 +1,44 @@ +package com.getcode.view.main.chat.list + +import androidx.paging.PagingData +import androidx.paging.map +import com.getcode.model.Conversation +import com.getcode.network.ConversationController +import com.getcode.network.TipController +import com.getcode.view.BaseViewModel2 +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class ChatListViewModel @Inject constructor( + conversationController: ConversationController, +): BaseViewModel2( + initialState = State(), + updateStateForEvent = updateStateForEvent +) { + data class State( + val x: String = "" + ) + + sealed interface Event { + data object Noop: Event + } + + + companion object { + val updateStateForEvent: (Event) -> ((State) -> State) = { event -> + when (event) { + Event.Noop -> { state -> state } + } + } + } +} + +data class ConversationWithMetadata( + val conversation: Conversation, + val image: String?, + val latestMessage: String?, + val latestMessageMillis: Long? +) \ No newline at end of file diff --git a/app/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt b/app/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt index f7167f5b0..d1397c9b4 100644 --- a/app/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt +++ b/app/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt @@ -190,7 +190,7 @@ private fun Header() { modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x2), ) Text( - text = stringResource(R.string.title_getKin), + text = stringResource(R.string.title_getCash), style = CodeTheme.typography.displayMedium.bolded(), modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x3), ) diff --git a/app/src/main/java/com/getcode/view/main/home/DecorView.kt b/app/src/main/java/com/getcode/view/main/home/DecorView.kt index 763c55829..e34ba1db1 100644 --- a/app/src/main/java/com/getcode/view/main/home/DecorView.kt +++ b/app/src/main/java/com/getcode/view/main/home/DecorView.kt @@ -59,7 +59,7 @@ internal fun DecorView( isCameraReady: Boolean, isPaused: Boolean, modifier: Modifier = Modifier, - showBottomSheet: (HomeBottomSheet) -> Unit, + onAction: (HomeAction) -> Unit, ) { val tips = LocalTipsEngine.current!!.tips as DefinedTips val tipProvider = LocalTipProvider.current @@ -74,7 +74,7 @@ internal fun DecorView( val scope = rememberCoroutineScope() val openDownloadModal = { - showBottomSheet(HomeBottomSheet.SHARE_DOWNLOAD) + onAction(HomeAction.SHARE_DOWNLOAD) scope.launch { delay(300) tipProvider.dismiss() @@ -113,7 +113,7 @@ internal fun DecorView( .align(Alignment.TopEnd) .clip(CircleShape) .rememberedClickable { - showBottomSheet(HomeBottomSheet.ACCOUNT) + onAction(HomeAction.ACCOUNT) }, painter = painterResource( R.drawable.ic_home_options @@ -186,7 +186,7 @@ internal fun DecorView( .padding(bottom = CodeTheme.dimens.grid.x3), state = dataState, onPress = { - showBottomSheet(it) + onAction(it) }, ) } diff --git a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt index 5ca35885e..977e07c67 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -42,12 +41,15 @@ import com.getcode.LocalBiometricsState import com.getcode.R import com.getcode.manager.TopBarManager import com.getcode.models.Bill +import com.getcode.models.DeepLinkRequest import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.core.LocalCodeNavigator import com.getcode.navigation.screens.AccountModal import com.getcode.navigation.screens.BalanceModal import com.getcode.navigation.screens.BuyMoreKinModal import com.getcode.navigation.screens.BuySellScreen +import com.getcode.navigation.screens.ChatListModal +import com.getcode.navigation.screens.ConnectAccount import com.getcode.navigation.screens.EnterTipModal import com.getcode.navigation.screens.GetKinModal import com.getcode.navigation.screens.GiveKinModal @@ -68,7 +70,6 @@ import com.getcode.view.main.home.components.PermissionsBlockingView import com.getcode.view.main.home.components.ReceivedKinConfirmation import com.getcode.view.main.home.components.TipConfirmation import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -76,20 +77,22 @@ import timber.log.Timber import kotlin.time.Duration.Companion.milliseconds -enum class HomeBottomSheet { +enum class HomeAction { NONE, ACCOUNT, GIVE_KIN, GET_KIN, BALANCE, - SHARE_DOWNLOAD + SHARE_DOWNLOAD, + TIP_CARD, + CHAT } @Composable fun HomeScreen( homeViewModel: HomeViewModel, - deepLink: String? = null, - requestPayload: String? = null, + cashLink: String? = null, + request: DeepLinkRequest? = null, ) { val navigator = LocalCodeNavigator.current val dataState by homeViewModel.uiFlow.collectAsState() @@ -107,8 +110,8 @@ fun HomeScreen( HomeScan( homeViewModel = homeViewModel, dataState = dataState, - deepLink = deepLink, - requestPayload = requestPayload, + cashLink = cashLink, + request = request, ) val context = LocalContext.current @@ -127,15 +130,6 @@ fun HomeScreen( } .launchIn(this) } - - LaunchedEffect(navigator) { - // reset tip entry state when tip entry is manually dismissed - // without advancing next - snapshotFlow { navigator.progress } - .filter { it == 0f && navigator.lastModalItem is EnterTipModal } - .onEach { homeViewModel.cancelTipEntry() } - .launchIn(this) - } } } } @@ -144,8 +138,8 @@ fun HomeScreen( private fun HomeScan( homeViewModel: HomeViewModel, dataState: HomeUiModel, - deepLink: String?, - requestPayload: String?, + cashLink: String?, + request: DeepLinkRequest?, ) { val navigator = LocalCodeNavigator.current val scope = rememberCoroutineScope() @@ -167,41 +161,49 @@ private fun HomeScan( val focusManager = LocalFocusManager.current - var deepLinkSaved by remember(deepLink) { - mutableStateOf(deepLink) + var cashLinkSaved by remember(cashLink) { + mutableStateOf(cashLink) } - var requestPayloadSaved by remember(requestPayload) { - mutableStateOf(requestPayload) + var requestPayloadSaved by remember(request) { + mutableStateOf(request) } val biometricsState = LocalBiometricsState.current - LaunchedEffect(biometricsState, previewing, dataState.balance, deepLinkSaved, requestPayloadSaved) { + LaunchedEffect(biometricsState, previewing, dataState.balance, cashLinkSaved, requestPayloadSaved) { if (previewing) { focusManager.clearFocus() } - if (biometricsState.passed && !deepLinkSaved.isNullOrBlank()) { - homeViewModel.openCashLink(deepLink) - deepLinkSaved = null + if (biometricsState.passed && !cashLinkSaved.isNullOrBlank()) { + homeViewModel.openCashLink(cashLink) + cashLinkSaved = null } - if (biometricsState.passed && !requestPayloadSaved.isNullOrBlank() && dataState.balance != null) { + if (biometricsState.passed && requestPayloadSaved != null && dataState.balance != null) { delay(500.milliseconds) - homeViewModel.handleRequest(requestPayload) + homeViewModel.handleRequest(request) requestPayloadSaved = null } } - fun showBottomSheet(bottomSheet: HomeBottomSheet) { + fun handleAction(action: HomeAction) { scope.launch { - when (bottomSheet) { - HomeBottomSheet.GIVE_KIN -> navigator.show(GiveKinModal) - HomeBottomSheet.ACCOUNT -> navigator.show(AccountModal) - HomeBottomSheet.GET_KIN -> navigator.show(GetKinModal) - HomeBottomSheet.BALANCE -> navigator.show(BalanceModal) - HomeBottomSheet.SHARE_DOWNLOAD -> navigator.show(ShareDownloadLinkModal) - HomeBottomSheet.NONE -> Unit + when (action) { + HomeAction.GIVE_KIN -> navigator.show(GiveKinModal) + HomeAction.ACCOUNT -> navigator.show(AccountModal) + HomeAction.GET_KIN -> navigator.show(GetKinModal) + HomeAction.BALANCE -> navigator.show(BalanceModal) + HomeAction.SHARE_DOWNLOAD -> navigator.show(ShareDownloadLinkModal) + HomeAction.TIP_CARD -> { + if (dataState.tipCardConnected) { + homeViewModel.presentShareableTipCard() + } else { + navigator.show(ConnectAccount()) + } + } + HomeAction.NONE -> Unit + HomeAction.CHAT -> navigator.show(ChatListModal) } } } @@ -225,7 +227,7 @@ private fun HomeScan( } ) }, - showBottomSheet = { showBottomSheet(it) }, + onAction = { handleAction(it) }, ) OnLifecycleEvent { _, event -> @@ -291,7 +293,7 @@ private fun BillContainer( homeViewModel: HomeViewModel, scannerView: @Composable () -> Unit, onStartCamera: () -> Unit, - showBottomSheet: (HomeBottomSheet) -> Unit, + onAction: (HomeAction) -> Unit, ) { val onPermissionResult = { isGranted: Boolean -> @@ -377,7 +379,7 @@ private fun BillContainer( exit = fadeOut(), modifier = Modifier.fillMaxSize() ) { - DecorView(updatedState, isCameraReady, isPaused) { showBottomSheet(it) } + DecorView(updatedState, isCameraReady, isPaused) { onAction(it) } } var managementHeight by remember { diff --git a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt index 9d13dfb94..e38d03990 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt @@ -31,7 +31,6 @@ import com.getcode.model.Kind import com.getcode.model.PrefsBool import com.getcode.model.Rate import com.getcode.model.RequestKinFeature -import com.getcode.model.TipCardFeature import com.getcode.model.TwitterUser import com.getcode.model.Username import com.getcode.models.Bill @@ -45,7 +44,7 @@ import com.getcode.models.PaymentValuation import com.getcode.models.TipConfirmation import com.getcode.models.amountFloored import com.getcode.network.BalanceController -import com.getcode.network.HistoryController +import com.getcode.network.ChatHistoryController import com.getcode.network.TipController import com.getcode.network.client.Client import com.getcode.network.client.RemoteSendException @@ -63,6 +62,7 @@ import com.getcode.network.client.sendRequestToReceiveBill import com.getcode.network.exchange.Exchange import com.getcode.network.repository.AppSettingsRepository import com.getcode.network.repository.BetaFlagsRepository +import com.getcode.network.repository.BetaOptions import com.getcode.network.repository.FeatureRepository import com.getcode.network.repository.PaymentRepository import com.getcode.network.repository.PrefRepository @@ -80,10 +80,11 @@ import com.getcode.util.resources.ResourceHelper import com.getcode.util.showNetworkError import com.getcode.util.vibration.Vibrator import com.getcode.utils.ErrorUtils -import com.getcode.utils.base64EncodedData +import com.getcode.utils.TraceType import com.getcode.utils.catchSafely import com.getcode.utils.network.NetworkConnectivityListener import com.getcode.utils.nonce +import com.getcode.utils.trace import com.getcode.vendor.Base58 import com.getcode.view.BaseViewModel import com.kik.kikx.models.ScannableKikCode @@ -146,7 +147,7 @@ data class HomeUiModel( val chatUnreadCount: Int = 0, val buyModule: Feature = BuyModuleFeature(), val requestKin: Feature = RequestKinFeature(), - val tips: Feature = TipCardFeature(), + val actions: List = listOf(HomeAction.GIVE_KIN, HomeAction.TIP_CARD, HomeAction.BALANCE), val tipCardConnected: Boolean = false, ) @@ -168,7 +169,7 @@ class HomeViewModel @Inject constructor( private val receiveTransactionRepository: ReceiveTransactionRepository, private val paymentRepository: PaymentRepository, private val balanceController: BalanceController, - private val historyController: HistoryController, + private val historyController: ChatHistoryController, private val tipController: TipController, private val prefRepository: PrefRepository, private val analytics: AnalyticsService, @@ -182,7 +183,7 @@ class HomeViewModel @Inject constructor( private val mnemonicManager: MnemonicManager, private val cashLinkManager: CashLinkManager, appSettings: AppSettingsRepository, - betaFlags: BetaFlagsRepository, + betaFlagsRepository: BetaFlagsRepository, features: FeatureRepository, ) : BaseViewModel(resources), ScreenModel { val uiFlow = MutableStateFlow(HomeUiModel()) @@ -204,21 +205,8 @@ class HomeViewModel @Inject constructor( } }.launchIn(viewModelScope) -// betaFlags.observe() -// .distinctUntilChanged() -// .onEach { beta -> -// ErrorUtils.setDisplayErrors(beta.displayErrors) -// -// if (beta.establishCodeRelationship) { -// val organizer = SessionManager.getOrganizer() ?: return@onEach -// val domain = Domain.from("getcode.com") ?: return@onEach -// if (organizer.relationshipFor(domain) == null) { -// client.awaitEstablishRelationship(organizer, domain) -// } -// } -// }.launchIn(viewModelScope) - features.buyModule + .distinctUntilChanged() .onEach { module -> uiFlow.update { it.copy(buyModule = module) @@ -226,22 +214,31 @@ class HomeViewModel @Inject constructor( }.launchIn(viewModelScope) features.requestKin + .distinctUntilChanged() .onEach { module -> uiFlow.update { it.copy(requestKin = module) } }.launchIn(viewModelScope) + betaFlagsRepository.observe() + .distinctUntilChanged() + .onEach { betaFlags -> + uiFlow.update { it.copy(actions = buildActions(betaFlags)) } + }.launchIn(viewModelScope) + tipController.showTwitterSplat .filter { it } .onEach { delay(500) } .flatMapLatest { tipController.connectedAccount } + .filter { tipController.verificationInProgress.value } .filterNotNull() .distinctUntilChanged() .filter { uiFlow.value.isCameraScanEnabled } .onEach { when (it) { is TwitterUser -> { + analytics.tipCardLinked() TopBarManager.showMessage( topBarMessage = TopBarManager.TopBarMessage( type = TopBarManager.TopBarMessageType.SUCCESS, @@ -376,6 +373,25 @@ class HomeViewModel @Inject constructor( } } + private fun buildActions( + betaOptions: BetaOptions, + ): List { + val actions = mutableListOf(HomeAction.GIVE_KIN) + actions += if (betaOptions.tipCardOnHomeScreen) { + HomeAction.TIP_CARD + } else { + HomeAction.GET_KIN + } + + if (betaOptions.conversationsEnabled) { + actions += HomeAction.CHAT + } + + actions += HomeAction.BALANCE + + return actions + } + fun onCameraScanning(scanning: Boolean) { uiFlow.update { it.copy(isCameraScanEnabled = scanning) } } @@ -446,7 +462,19 @@ class HomeViewModel @Inject constructor( ) }, onError = { cancelSend(style = PresentationStyle.Slide) }, - present = { data -> presentSend(data, bill, vibrate) } + present = { data -> + if (!bill.didReceive) { + trace( + tag = "Bill", + message = "Pull out cash", + metadata = { + "amount" to bill.amount + }, + type = TraceType.User, + ) + } + presentSend(data, bill, vibrate) + } ) } @@ -643,13 +671,41 @@ class HomeViewModel @Inject constructor( when (codePayload.kind) { Kind.Cash, - Kind.GiftCard -> attemptReceive(organizer, codePayload) + Kind.GiftCard -> { + trace( + tag = "Bill", + message = "Scanned cash", + type = TraceType.User, + ) + attemptReceive(organizer, codePayload) + } Kind.RequestPayment, - Kind.RequestPaymentV2 -> attemptPayment(codePayload) + Kind.RequestPaymentV2 -> { + trace( + tag = "Bill", + message = "Scanned request card", + type = TraceType.User, + ) + attemptPayment(codePayload) + } - Kind.Login -> attemptLogin(codePayload) - Kind.Tip -> attemptTip(codePayload) + Kind.Login -> { + trace( + tag = "Bill", + message = "Scanned login card", + type = TraceType.User, + ) + attemptLogin(codePayload) + } + Kind.Tip -> { + trace( + tag = "Bill", + message = "Scanned tip card", + type = TraceType.User, + ) + attemptTip(codePayload) + } } } @@ -685,12 +741,18 @@ class HomeViewModel @Inject constructor( tipController.clearTwitterSplat() + trace( + tag = "Bill", + message = "Show my tip card", + type = TraceType.User, + ) + withContext(Dispatchers.Main) { uiFlow.update { val billState = it.billState.copy( bill = Bill.Tip(code), primaryAction = BillState.Action.Share { onRemoteSend() }, - secondaryAction = BillState.Action.Done(::cancelSend) + secondaryAction = BillState.Action.Cancel(::cancelSend) ) it.copy( @@ -883,30 +945,28 @@ class HomeViewModel @Inject constructor( val isReceived = payload != null val presentationStyle = if (isReceived) PresentationStyle.Pop else PresentationStyle.Slide - withContext(Dispatchers.Main) { - uiFlow.update { - var billState = it.billState.copy( - bill = Bill.Payment(amount, code, request), - valuation = PaymentValuation(amount), - primaryAction = null, - ) - - if (isReceived) { - billState = billState.copy( - paymentConfirmation = PaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = code, - requestedAmount = amount, - localAmount = amount.replacing(exchange.localRate) - ), - ) - } + uiFlow.update { + var billState = it.billState.copy( + bill = Bill.Payment(amount, code, request), + valuation = PaymentValuation(amount), + primaryAction = null, + ) - it.copy( - presentationStyle = presentationStyle, - billState = billState, + if (isReceived) { + billState = billState.copy( + paymentConfirmation = PaymentConfirmation( + state = ConfirmationState.AwaitingConfirmation, + payload = code, + requestedAmount = amount, + localAmount = amount.replacing(exchange.localRate) + ), ) } + + it.copy( + presentationStyle = presentationStyle, + billState = billState, + ) } analytics.requestShown(amount = amount) @@ -929,17 +989,14 @@ class HomeViewModel @Inject constructor( cashLinkManager.cancelBillTimeout() val paymentConfirmation = uiFlow.value.billState.paymentConfirmation ?: return@launch - withContext(Dispatchers.Main) { - uiFlow.update { - val billState = it.billState - it.copy( - billState = billState.copy( - paymentConfirmation = paymentConfirmation.copy(state = ConfirmationState.Sending) - ), - ) - } + uiFlow.update { + val billState = it.billState + it.copy( + billState = billState.copy( + paymentConfirmation = paymentConfirmation.copy(state = ConfirmationState.Sending) + ), + ) } - runCatching { paymentRepository.completePayment( paymentConfirmation.requestedAmount, @@ -1358,10 +1415,9 @@ class HomeViewModel @Inject constructor( } private fun shareTipCard() = viewModelScope.launch { - val username = tipController.connectedAccount.value?.username ?: return@launch - + val connectedAccount = tipController.connectedAccount.value ?: return@launch withContext(Dispatchers.Main) { - val shareIntent = IntentUtils.tipCard(username) + val shareIntent = IntentUtils.tipCard(connectedAccount.username, connectedAccount.platform) _eventFlow.emit(HomeEvent.SendIntent(shareIntent)) } @@ -1430,9 +1486,7 @@ class HomeViewModel @Inject constructor( } } - fun handleRequest(bytes: String?) { - val data = bytes?.base64EncodedData() ?: return - val request = DeepLinkRequest.from(data) + fun handleRequest(request: DeepLinkRequest?) { if (request != null) { if (request.paymentRequest != null) { viewModelScope.launch { diff --git a/app/src/main/java/com/getcode/view/main/home/components/BillManagementOptions.kt b/app/src/main/java/com/getcode/view/main/home/components/BillManagementOptions.kt index e28b133fd..6ff73b582 100644 --- a/app/src/main/java/com/getcode/view/main/home/components/BillManagementOptions.kt +++ b/app/src/main/java/com/getcode/view/main/home/components/BillManagementOptions.kt @@ -21,6 +21,7 @@ import com.getcode.theme.Gray50 import com.getcode.theme.White import com.getcode.ui.components.CodeCircularProgressIndicator import com.getcode.ui.components.Pill +import com.getcode.ui.utils.addIf import com.getcode.ui.utils.rememberedClickable @Composable @@ -45,10 +46,9 @@ internal fun BillManagementOptions( if (primaryAction != null) { Pill( modifier = Modifier - .rememberedClickable(enabled = !isSending) { primaryAction.action() } - .padding(vertical = 15.dp, horizontal = 20.dp), - contentPadding = PaddingValues(), - backgroundColor = Gray50, + .rememberedClickable(enabled = !isSending) { primaryAction.action() }, + contentPadding = PaddingValues(15.dp), + backgroundColor = CodeTheme.colors.action, ) { Box { Row( @@ -59,10 +59,12 @@ internal fun BillManagementOptions( contentDescription = "", modifier = Modifier.width(22.dp) ) - Text( - modifier = Modifier.padding(start = 10.dp), - text = primaryAction.label - ) + primaryAction.label?.let { label -> + Text( + modifier = Modifier.padding(start = 10.dp), + text = label + ) + } } if (isSending) { @@ -80,20 +82,21 @@ internal fun BillManagementOptions( if (secondaryAction != null) { Pill( modifier = Modifier - .rememberedClickable(enabled = isInteractable) { secondaryAction.action() } - .padding(vertical = 15.dp, horizontal = 20.dp), - contentPadding = PaddingValues(), - backgroundColor = Gray50, + .rememberedClickable(enabled = isInteractable) { secondaryAction.action() }, + contentPadding = PaddingValues(15.dp), + backgroundColor = CodeTheme.colors.action, ) { Image( painter = secondaryAction.asset, contentDescription = "", modifier = Modifier.size(18.dp) ) - Text( - modifier = Modifier.padding(start = 10.dp), - text = secondaryAction.label - ) + secondaryAction.label?.let { label -> + Text( + modifier = Modifier.padding(start = 10.dp), + text = label + ) + } } } } diff --git a/app/src/main/java/com/getcode/view/main/home/components/HomeBottom.kt b/app/src/main/java/com/getcode/view/main/home/components/HomeBottom.kt index 0f9419543..25e7645f2 100644 --- a/app/src/main/java/com/getcode/view/main/home/components/HomeBottom.kt +++ b/app/src/main/java/com/getcode/view/main/home/components/HomeBottom.kt @@ -4,10 +4,10 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.scaleIn import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -15,8 +15,9 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.res.painterResource @@ -24,15 +25,17 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach import com.getcode.R import com.getcode.theme.CodeTheme import com.getcode.ui.components.Badge import com.getcode.ui.components.Row import com.getcode.ui.components.chat.ChatNodeDefaults import com.getcode.ui.utils.heightOrZero -import com.getcode.ui.utils.rememberedClickable +import com.getcode.ui.utils.unboundedClickable import com.getcode.ui.utils.widthOrZero -import com.getcode.view.main.home.HomeBottomSheet +import com.getcode.util.resources.icons.AutoMirroredMessageCircle +import com.getcode.view.main.home.HomeAction import com.getcode.view.main.home.HomeUiModel @Preview @@ -40,56 +43,85 @@ import com.getcode.view.main.home.HomeUiModel internal fun HomeBottom( modifier: Modifier = Modifier, state: HomeUiModel = HomeUiModel(), - onPress: (homeBottomSheet: HomeBottomSheet) -> Unit = {}, + onPress: (homeBottomSheet: HomeAction) -> Unit = {}, ) { Row( modifier = Modifier .fillMaxWidth() .then(modifier), verticalAlignment = Alignment.Bottom, - contentPadding = PaddingValues(horizontal = CodeTheme.dimens.grid.x3), + horizontalArrangement = Arrangement.SpaceAround, ) { - BottomBarAction( - label = stringResource(R.string.title_getKin), - contentPadding = PaddingValues( - start = CodeTheme.dimens.grid.x3, - end = CodeTheme.dimens.grid.x3, - top = CodeTheme.dimens.grid.x1, - bottom = CodeTheme.dimens.grid.x2, - ), - imageSize = CodeTheme.dimens.grid.x7, - painter = painterResource(R.drawable.ic_wallet), - onClick = { onPress(HomeBottomSheet.GET_KIN) }, - ) - Spacer(modifier = Modifier.weight(1f)) - BottomBarAction( - label = stringResource(R.string.action_giveKin), - contentPadding = PaddingValues( - horizontal = CodeTheme.dimens.grid.x3, - vertical = CodeTheme.dimens.grid.x2 - ), - imageSize = CodeTheme.dimens.grid.x10, - painter = painterResource(R.drawable.ic_kin_white), - onClick = { onPress(HomeBottomSheet.GIVE_KIN) } - ) - Spacer(modifier = Modifier.weight(1f)) - BottomBarAction( - label = stringResource(R.string.action_balance), - contentPadding = PaddingValues( - horizontal = CodeTheme.dimens.grid.x2, - ), - imageSize = CodeTheme.dimens.grid.x9, - painter = painterResource(R.drawable.ic_history), - onClick = { onPress(HomeBottomSheet.BALANCE) }, - badge = { - Badge( - modifier = Modifier.padding(top = 2.dp, end = 2.dp), - count = state.chatUnreadCount, - color = ChatNodeDefaults.UnreadIndicator, - enterTransition = scaleIn(animationSpec = tween(durationMillis = 300, delayMillis = 1000)) + fadeIn() - ) + state.actions.fastForEach { action -> + when (action) { + HomeAction.GIVE_KIN -> { + BottomBarAction( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_give), + painter = painterResource(R.drawable.ic_kin_white_small), + onClick = { onPress(action) } + ) + } + + HomeAction.GET_KIN -> { + BottomBarAction( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_receive), + painter = painterResource(R.drawable.ic_wallet), + onClick = { onPress(action) }, + ) + } + + HomeAction.BALANCE -> { + BottomBarAction( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_balance), + painter = painterResource(R.drawable.ic_balance), + onClick = { onPress(HomeAction.BALANCE) }, + badge = { + Badge( + modifier = Modifier.padding(top = 2.dp, end = 2.dp), + count = state.chatUnreadCount, + color = ChatNodeDefaults.UnreadIndicator, + enterTransition = scaleIn( + animationSpec = tween( + durationMillis = 300, + delayMillis = 1000 + ) + ) + fadeIn() + ) + } + ) + } + + HomeAction.TIP_CARD -> { + BottomBarAction( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_receive), + painter = painterResource(R.drawable.ic_tip_card), + onClick = { onPress(action) }, + ) + } + + HomeAction.CHAT -> { + BottomBarAction( + modifier = Modifier.weight(1f), + label = stringResource(R.string.action_chat), + painter = rememberVectorPainter(AutoMirroredMessageCircle), + onClick = { onPress(action) }, + ) + } + + else -> { + BottomBarAction( + modifier = Modifier.weight(1f).alpha(0f), + label = "", + painter = painterResource(R.drawable.ic_tip_card), + onClick = null, + ) + } } - ) + } } } @@ -97,19 +129,20 @@ internal fun HomeBottom( private fun BottomBarAction( modifier: Modifier = Modifier, label: String, - contentPadding: PaddingValues = PaddingValues(), + contentPadding: PaddingValues = PaddingValues( + vertical = CodeTheme.dimens.grid.x2 + ), painter: Painter, - imageSize: Dp, + imageSize: Dp = CodeTheme.dimens.staticGrid.x10, badge: @Composable () -> Unit = { }, - onClick: () -> Unit, + onClick: (() -> Unit)?, ) { Layout( modifier = modifier, content = { Column( modifier = Modifier - .clip(CodeTheme.shapes.medium) - .rememberedClickable { onClick() } + .unboundedClickable(enabled = onClick != null, rippleRadius = imageSize) { onClick?.invoke() } .layoutId("action"), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -138,7 +171,7 @@ private fun BottomBarAction( measurables.find { it.layoutId == "badge" }?.measure(constraints) val maxWidth = widthOrZero(actionPlaceable) - val maxHeight = heightOrZero(actionPlaceable) // + heightOrZero(badgePlaceable) / 2 + val maxHeight = heightOrZero(actionPlaceable) layout( width = maxWidth, height = maxHeight, @@ -150,4 +183,5 @@ private fun BottomBarAction( ) } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt b/app/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt index de11719c9..4d09eb4f1 100644 --- a/app/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt +++ b/app/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt @@ -145,7 +145,7 @@ private fun TweetPreview( modifier = Modifier.weight(1f), text = xMessage, color = Color.White, - style = CodeTheme.typography.textSmall + style = CodeTheme.typography.textSmall, ) } } diff --git a/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt b/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt index bc32fe3d2..852e1a916 100644 --- a/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt @@ -69,6 +69,7 @@ class TipConnectViewModel @Inject constructor( .map { IntentUtils.tweet(it) } .onEach { dispatchEvent(Event.OpenX(it)) + tipController.startVerification() }.launchIn(viewModelScope) } diff --git a/app/src/main/res/drawable/ic_balance.xml b/app/src/main/res/drawable/ic_balance.xml new file mode 100644 index 000000000..134e269a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_balance.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_chat.xml b/app/src/main/res/drawable/ic_chat.xml new file mode 100644 index 000000000..d1d8828da --- /dev/null +++ b/app/src/main/res/drawable/ic_chat.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_kin_white_small.xml b/app/src/main/res/drawable/ic_kin_white_small.xml new file mode 100644 index 000000000..471856956 --- /dev/null +++ b/app/src/main/res/drawable/ic_kin_white_small.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_tip_card.xml b/app/src/main/res/drawable/ic_tip_card.xml new file mode 100644 index 000000000..50e4af53d --- /dev/null +++ b/app/src/main/res/drawable/ic_tip_card.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/values-ar/strings-localized.xml b/app/src/main/res/values-ar/strings-localized.xml index 834714080..f934b233f 100644 --- a/app/src/main/res/values-ar/strings-localized.xml +++ b/app/src/main/res/values-ar/strings-localized.xml @@ -1,5 +1,6 @@ + أضف نقودًا عن طريق استخدام بطاقة الخصم السماح بإتاحة الوصول إلى الكاميرا السماح بإتاحة الوصول إلى جهات الاتصال السماح بالإشعارات اللحظية @@ -23,6 +24,7 @@ تفعيل خاصية التعرّف على الوجه تفعيل خاصية التعرّف على بصمة الإصبع الخروج + أعط منح الأموال الدعوة الدعوات @@ -43,6 +45,7 @@ اللصق من الحافظة انشر لربط حساب الوضع في المحفظة + استقبل استعادة الحساب الموجود بالفعل التذكير حذف رقم الهاتف @@ -53,6 +56,7 @@ تم الإرسال إرسال رمز التحقق مشاركة + مشاركة كعنوان URL شارك رابط التنزيل شارك مقطع الفيديو هذا عرض بطاقة الإكرامية الخاصة بي @@ -322,9 +326,11 @@ فشلت العملية الأسئلة الشائعة الممنوحة + احصل غلى نقود احصل على صديق بدأ في Code احصل على Kin احصل على المزيد من Kin + أعط النقود منح الأموال الرصيد غير كافٍ دعوة صديق @@ -344,6 +350,7 @@ العملات الأخيرة ارسل طلب إحالة لصديق, واحصل على 5 دولارات مكافأة الإحالة + طلب نقود اطلب Kin اطلب إكرامية مطلوب Face ID\n\n @@ -358,6 +365,7 @@ النقود المصروفة تبديل الحسابات شروط الخدمة + بطاقة Tip إكرامية Kin غير معروف التحديث مطلوب diff --git a/app/src/main/res/values-bg/strings-localized.xml b/app/src/main/res/values-bg/strings-localized.xml index 9755ba5fd..02a8a7c78 100644 --- a/app/src/main/res/values-bg/strings-localized.xml +++ b/app/src/main/res/values-bg/strings-localized.xml @@ -1,5 +1,6 @@ + Добавете парична сума с дебитна карта Разреши достъп до камерата Разреши достъп до контактите Разреши насочени известия @@ -23,6 +24,7 @@ Разрешете лицево разпознаване Разрешете разпознаване чрез докосване Изход + Дайте Задай член на семейството Покани Покани @@ -43,6 +45,7 @@ Постави от клипборда Публикувайте за да свържете акаунт Постави в портфейл + Получете Възстанови съществуващ профил Напомняне Премахване на телефонен номер @@ -53,6 +56,7 @@ Изпращане Изпрати код за потвърждение Сподели + Споделете като URL Споделете линк за сваляне Споделете този видео клип Покажи моята карта за бакшиши @@ -322,9 +326,11 @@ Неуспешно Често задавани въпроси Даде + Вземете парична сума Вземете \"Приятел се присъедини\" към Code Получаване на средства в Kin Получавайте повече Kin + Дайте кеш Посочете член на семейството Недостатъчна наличност Покани приятел @@ -344,6 +350,7 @@ Последно използвани валути Препоръчай на приятел, вземи $5 Бонус за препоръка + Направете заявка за парична сума Поискай Kin Поискай бакшиш Изискване на Face ID @@ -358,6 +365,7 @@ Похарчено Смени акаунти Условия на ползване + Карта за бакшиши Бакшиш Kin Неизвестен Необходимо е обновяване diff --git a/app/src/main/res/values-cs/strings-localized.xml b/app/src/main/res/values-cs/strings-localized.xml index 1b5017e4f..9f5882a99 100644 --- a/app/src/main/res/values-cs/strings-localized.xml +++ b/app/src/main/res/values-cs/strings-localized.xml @@ -1,5 +1,6 @@ + Přidání hotovosti pomocí debetní karty Umožnit přístup fotoaparátu Povolit přístup ke kontaktům Povolit zprávy push @@ -23,6 +24,7 @@ Povolit Face ID Povolit Touch ID Odejít + Dát Darovat kin Přizvat Pozvánky @@ -43,6 +45,7 @@ Vložit z vývěsky Zveřejnit příspěvek pro připojení účtu Vložit do peněženky + Přijmout Obnovit stávající účet Připomenout Odstranit telefonní číslo @@ -53,6 +56,7 @@ Odeslat Zaslat ověřovací kód Sdílet + Sdílet jako adresu URL Sdílet odkaz stažení Sdílet toto video Ukázat kartu spropitného @@ -322,9 +326,11 @@ Selhalo Časté dotazy Darováno + Získat hotovost Začněte kamarádem používat Code Získat Kin Získejte další Kin + Dát hotovost Darovat kin Nedostatek prostředků Přizvat přítele @@ -344,6 +350,7 @@ Poslední měny Doporučte kamaráda a získejte 5 USD Bonus za doporučení + Žádost o hotovost Požádat o Kin Požádat o spropitné Vyžaduje Face ID @@ -358,6 +365,7 @@ Utraceno Přepnout účet Všeobecné obchodní podmínky + Karta tipu Dát Kin spropitné Neznámé Je nutná aktualizace diff --git a/app/src/main/res/values-da/strings-localized.xml b/app/src/main/res/values-da/strings-localized.xml index cb7e5fd4a..7a5e106f3 100644 --- a/app/src/main/res/values-da/strings-localized.xml +++ b/app/src/main/res/values-da/strings-localized.xml @@ -1,5 +1,6 @@ + Tilføj kontanter med et betalingskort Tillad kameraadgang Tillad adgang til kontakter Tillad pushnotifikationer @@ -23,6 +24,7 @@ Aktiver Face ID Aktiver Touch ID Forlad + Giv Giv Kin Inviter Invitationer @@ -43,6 +45,7 @@ Indsæt fra udklipsholder Slå noget op for at forbinde konto Læg i Wallet + Modtag Genskab eksisterende konto Husk Fjern telefonnummer @@ -53,6 +56,7 @@ Send Send verifikationskode Del + Del som en URL Del downloadlink Del denne video Vis mit drikkepengekort @@ -322,9 +326,11 @@ Mislykkedes FAQ Gav + Få kontanter Få en ven i gang med Code Få Kin Få flere Kin + Giv kontanter Giv Kin Ikke nok penge Inviter en ven @@ -344,6 +350,7 @@ Seneste valutaer Henvis en ven, få $5 Henvisningsbonus + Anmod om kontanter Anmod om Kin Anmod om drikkepenge Kræv Face ID @@ -358,6 +365,7 @@ Brugt Skift konti Servicebetingelser + Tipkort Giv drikkepenge i Kin Ukendt Opdatering nødvendig diff --git a/app/src/main/res/values-de/strings-localized.xml b/app/src/main/res/values-de/strings-localized.xml index 37b5c6613..1a84216c3 100644 --- a/app/src/main/res/values-de/strings-localized.xml +++ b/app/src/main/res/values-de/strings-localized.xml @@ -1,5 +1,6 @@ + Bargeld mit einer Debitkarte hinzufügen\n Kamera-Zugriff erlauben Zugriff auf Kontakte zulassen Push-Benachrichtigungen zulassen @@ -23,6 +24,7 @@ Face ID aktivieren Touch ID aktivieren Beenden + Geben Kin überweisen Einladen Einladen @@ -43,6 +45,7 @@ Aus Zwischenablage einfügen Posten, um das Konto zu verbinden In Wallet legen + Erhalten Bestehendes Konto wiederherstellen Erinnern Telefonnummer entfernen @@ -53,6 +56,7 @@ Senden Verifizierungscode senden Teilen + Als URL teilen Download-Link teilen Dieses Video teilen\n Meine Trinkgeldkarte anzeigen @@ -322,9 +326,11 @@ Fehlgeschlagen FAQ Überwiesen + Bargeld erhalten\n Gewinne einen deiner Freunde für Code Kin erhalten Mehr Kin erhalten\n + Geld geben Kin überweisen Unzureichende Mittel\n Einen Freund einladen @@ -344,6 +350,7 @@ Letzte Währungen Empfehle Sie uns an einen Freund oder eine Freundin weiter und erhalten Sie 5 $ dafür Empfehlungsbonus + Bargeld anfordern Kin anfordern Trinkgeld anfordern Face ID erforderlich @@ -358,6 +365,7 @@ Ausgegeben Konten wechseln Servicebedingungen + Tippkarte Kin-Trinkgeld geben Unbekannt Aktualisierung erforderlich diff --git a/app/src/main/res/values-el/strings-localized.xml b/app/src/main/res/values-el/strings-localized.xml index 911192c75..32d87ad02 100644 --- a/app/src/main/res/values-el/strings-localized.xml +++ b/app/src/main/res/values-el/strings-localized.xml @@ -1,5 +1,6 @@ + Προσθέστε μετρητά με χρεωστική κάρτα Να Επιτρέπεται η Πρόσβαση στην Κάμερα Να Επιτρέπεται η Πρόσβαση στις Επαφές Να Επιτρέπονται οι Ειδοποιήσεις Push @@ -23,6 +24,7 @@ Ενεργοποιήστε το Face ID Ενεργοποιήστε το Touch ID Έξοδος + Δώσε \n Δώστε Kin Πρόσκληση Προσκλήσεις @@ -43,6 +45,7 @@ Επικόλληση από Πρόχειρο Στείλε Μήνυμα για Σύνδεση του Λογαριασμού Τοποθέτηση σε Πορτοφόλι + Λάβε Ανάκτηση Υπάρχοντος Λογαριασμού Υπενθύμιση Κατάργηση Αριθμού Τηλεφώνου @@ -53,6 +56,7 @@ Αποστολή Αποστολή Κωδικού Επαλήθευσης Κοινοποίηση + Κοινή χρήση ως URL(Ενιαίος Εντοπιστής Πόρων) Μοιράσου το Σύνδεσμο Λήψης Μοιραστείτε Αυτό το Βίντεο Κοινοποίηση της Κάρτας Φιλοδωρημάτων μου @@ -322,9 +326,11 @@ Απέτυχε Αριθμός Τηλεφώνου Δόθηκε + Λάβετε μετρητά Βρες ένα Φίλο να Ξεκινήσει στο Code Λάβετε Kin Αποκτήστε Περισσότερα Kin + Δώσε Μετρητά Δώστε Kin Ανεπαρκή Κεφάλαια Προσκαλέστε έναν Φίλο @@ -344,6 +350,7 @@ Πρόσφατα Νομίσματα Σύστησε ένα Φίλο, Λάβε $5 Δώρο Σύστασης Φίλου + Ζητήστε μετρητά Ζητήστε Kin Ζητήστε Φιλοδώρημα Απαιτείται Αναγνώριση Προσώπου @@ -358,6 +365,7 @@ Δαπανημένα Εναλλαγή Λογαριασμών Όροι Χρήσης + Συμβουλή Φιλοδώρημα Kin Άγνωστο Απαιτείται Ενημέρωση diff --git a/app/src/main/res/values-en-rGB/strings-localized.xml b/app/src/main/res/values-en-rGB/strings-localized.xml index bdfac5686..2daec51b1 100644 --- a/app/src/main/res/values-en-rGB/strings-localized.xml +++ b/app/src/main/res/values-en-rGB/strings-localized.xml @@ -1,11 +1,12 @@ + Add Cash with a Debit Card Allow Camera Access Allow Access to Contacts Allow Push Notifications Balance - Add Cash with a Debit Card - Add Cash with a Debit Card + Buy Kin + Buy More Kin Cancel Cancel Send Collect This Cash @@ -23,7 +24,8 @@ Enable Face ID Enable Touch ID Exit - Give Cash + Give + Give Kin Invite Invites Join the Waitlist @@ -43,6 +45,7 @@ Paste From Clipboard Post to Connect Account Put in Wallet + Receive Recover Existing Account Remind Remove Phone Number @@ -53,6 +56,7 @@ Send Send Verification Code Share + Share as a URL Share Download Link Share this video Show My Tip Card @@ -68,10 +72,10 @@ Unsubscribe Update View Access Key - Withdraw Cash + Withdraw Kin Wrote the 12 Words Down Instead? Yes - Yes, Withdraw Cash + Yes, Withdraw Kin Yes, I Wrote Them Down and Kin @@ -213,9 +217,9 @@ Tap the Google Lens icon to open the QR code to log into Code. Alternatively you can log in manually by entering the 12 words in the Code Log In screen. Advarsel! Dette billede giver adgang til alle de funds, du har i Code. Del ikke dette billede med andre. Hold det sikkert og beskyttet. Code enables you to receive Kin by pointing your camera at the digital bill on another user\'s phone - You need to turn on Camera in Settings to scan Codes + You need to allow camera access to be able to receive Kin Authenticate to access Code. - Add Cash with a Debit Card + Buy Kin Buy Kin (Coming Soon) Buying and selling Kin is currently a complex process. These processes will become simpler over time. If you would like to learn how to buy and sell Kin today, you can watch the walkthrough videos below. You can only give up to %1$s @@ -322,10 +326,12 @@ Failed FAQ Gave + Get Cash Get a Friend Started on Code - Get Cash - Get more cash - Give Cash + Get Kin + Get more Kin + Give Cash + Give Kin Insufficient funds Invite a Friend Limited Time Offer @@ -344,7 +350,8 @@ Recent Currencies Refer a Friend and Get $5 Referral Bonus - Request Cash + Request Cash + Request Kin Request a Tip Require Face ID Require Passcode @@ -358,12 +365,13 @@ Spent Switch Accounts Terms of Service - Tip Cash + Tip Card + Tip Kin Unknown Update Required Verify Phone Number Welcome Bonus - Withdraw Cash + Withdraw Kin Withdrew Your Access Key Tap the logo to share the app download link diff --git a/app/src/main/res/values-es/strings-localized.xml b/app/src/main/res/values-es/strings-localized.xml index 0287edda8..d67476c18 100644 --- a/app/src/main/res/values-es/strings-localized.xml +++ b/app/src/main/res/values-es/strings-localized.xml @@ -1,5 +1,6 @@ + Añadir efectivo con una tarjeta de débito Permitir acceso a cámara Permitir acceso a contactos Permitir notificaciones Push @@ -23,6 +24,7 @@ Habilitar identificación facial Habilitar identificación táctil Salir + Dar Dar Kin Invitar Invitaciones @@ -43,6 +45,7 @@ Pegar desde el portapapeles Publica para conectar la cuenta Guardar en el monedero + Recibir Recuperar cuenta existente Recordar Eliminar número de teléfono @@ -53,6 +56,7 @@ Enviar Enviar código de verificación Compartir + Compartir como URL Compartir enlace de descarga Compartir este vídeo Mostrar mi tarjeta de propinas @@ -322,9 +326,11 @@ Fallido FAQ Dio + Obtener efectivo Inicia a un amigo en Code Obtener Kin Obtenga más Kin + Dar efectivo Dar Kin Fondos insuficientes Invitar a un amigo @@ -344,6 +350,7 @@ Divisas recientes Recomienda a un amigo y consigue 5 $ Bono por recomendación + Solicitar efectivo Solicitar Kin Solicitar una propina Requerir Face ID @@ -358,6 +365,7 @@ Gastado Cambiar de cuenta Términos y condiciones + Tarjeta de propina Dar Kin de propina Desconocido Actualización requerida diff --git a/app/src/main/res/values-fi/strings-localized.xml b/app/src/main/res/values-fi/strings-localized.xml index 7c77251f0..3bdb4ccf3 100644 --- a/app/src/main/res/values-fi/strings-localized.xml +++ b/app/src/main/res/values-fi/strings-localized.xml @@ -1,5 +1,6 @@ + Lisää rahaa pankkikortilla Salli kameran käyttöoikeudet Salli yhteystietojen käyttöoikeudet Salli palveluilmoitukset @@ -23,6 +24,7 @@ Ota kasvotunnistus käyttöön Ota kosketustunnistus käyttöön Poistu + Anna Anna kinejä Kutsu Kutsut @@ -43,6 +45,7 @@ Liitä leikepöydältä Julkaise yhdistettävälle tilille Pane lompakkoon + Ota vastaan Palauta olemassa oleva tili Muistuta Poista puhelinnumero @@ -53,6 +56,7 @@ Lähetä Lähetä vahvistuskoodi Jaa + Jaa URL-osoitteena Jaa latauslinkki Jaa tämä video Näytä tippikorttini @@ -322,9 +326,11 @@ Epäonnistui UKK Antoi + Nosta rahaa Auta kaveri alkuun Coden parissa Hae Kin Hanki lisää kinejä + Anna rahaa Antoi kinejä Riittämättömästi varoja Kutsu ystävä @@ -344,6 +350,7 @@ Viimeaikaiset valuutat Suosita ystävää, saat 5 $ Suositusbonus + Pyydä rahaa Pyydä kinejä Pyydä tippi Vaadi kasvotunnistus @@ -358,6 +365,7 @@ Käytetty Vaihda tilejä Palveluehdot + Tippikortti Anna kinejä tippinä Tuntematon Päivitys vaaditaan diff --git a/app/src/main/res/values-fr-rCA/strings-localized.xml b/app/src/main/res/values-fr-rCA/strings-localized.xml index 7054916d7..3b873ba65 100644 --- a/app/src/main/res/values-fr-rCA/strings-localized.xml +++ b/app/src/main/res/values-fr-rCA/strings-localized.xml @@ -1,5 +1,6 @@ + Ajouter des liquidités avec une carte de débit Autoriser l\'accès à la caméra Autoriser l\'accès aux contacts Autoriser les notifications poussées @@ -23,6 +24,7 @@ Activer Face ID Activer Touch ID Sortir + Donner Donner des Kin Inviter Invitations @@ -43,6 +45,7 @@ Coller depuis le presse-papiers Envoyez un message à Connecter le compte Mettre dans le portefeuille + Recevoir Récupérer un compte existant Rappeler Supprimer le numéro de téléphone @@ -53,6 +56,7 @@ Envoyer Envoyer le code de vérification Partager + Partager comme adresse URL Partager le lien de téléchargement Partagez cette vidéo Montrer ma carte de pourboire @@ -322,9 +326,11 @@ Échoué FAQ A donné + Obtenir des liquidités Faire démarrer un ami sur Code Obtenez Kin Obtenez plus de Kin + Donner de l\'argent Donner Kin Fonds insuffisants Inviter un ami @@ -344,6 +350,7 @@ Devises récentes Parrainez un ami, recevez 5 $ Prime de parrainage + Demander des liquidités Demander des Kin Demander un pourboire Exiger Face ID @@ -358,6 +365,7 @@ Dépensé Changer de compte Conditions d\'utilisation + Carte à puce Donner un pourboire en Kin Inconnu Mise à jour requise diff --git a/app/src/main/res/values-fr/strings-localized.xml b/app/src/main/res/values-fr/strings-localized.xml index 186f8078a..f6f5e1835 100644 --- a/app/src/main/res/values-fr/strings-localized.xml +++ b/app/src/main/res/values-fr/strings-localized.xml @@ -1,5 +1,6 @@ + Ajouter des liquidités avec une carte de débit Autoriser l\'accès à la caméra Autoriser l\'accès aux contacts Autoriser les notifications push @@ -23,6 +24,7 @@ Activez Face ID Activez Touch ID Quitter + Donner Donner Kin Inviter Invitations @@ -43,6 +45,7 @@ Coller depuis le presse-papiers Publier sur le compte connecté Mettre dans le portefeuille + Recevoir Récupérer le compte existant Rappeler Supprimer le numéro de téléphone @@ -53,6 +56,7 @@ Envoyer Envoyer le code de vérification Partager + Partager comme URL Partagez le lien de téléchargement Partager cette vidéo Afficher ma carte de pourboires @@ -322,9 +326,11 @@ Échoué FAQ Donné + Obtenir des liquidités Invitez un ami à s\'inscrire sur Code Obtenir des Kin Obtenir plus de Kin + Donner de l\'argent Donner Kin Fonds insuffisants Inviter un ami @@ -344,6 +350,7 @@ Devises récentes Parrainez un ami, recevez 5 $ Bonus de parrainage + Demander des liquidités Demander des Kin Demander un pourboire Nécessite Face ID @@ -358,6 +365,7 @@ Dépensé Échanger des comptes Conditions d\'utilisation + Carte conseil Donner un pourboire en Kin Inconnu Mise à jour requise diff --git a/app/src/main/res/values-he/strings-localized.xml b/app/src/main/res/values-he/strings-localized.xml index d92d38f62..5e64d72d2 100644 --- a/app/src/main/res/values-he/strings-localized.xml +++ b/app/src/main/res/values-he/strings-localized.xml @@ -1,5 +1,6 @@ + הוספת כסף באמצעות כרטיס חיוב אפשר גישה למצלמה אפשר גישה לאנשי הקשר אפשר הודעות דחיפה @@ -23,6 +24,7 @@ הפוך Face ID לזמין הפוך Touch ID לזמין יציאה + תן תן קין הזמן הזמנות @@ -43,6 +45,7 @@ הדבק מהלוח יש להעלות פוסט כדי לחבר את החשבון שים בארנק + קבל שחזר חשבון קיים הזכר הסר מספר טלפון @@ -53,6 +56,7 @@ שליחה שלח קוד אימות שיתוף + שיתוף כ-URL שתף קישור להורדה שתפו סרטון זה הצג את כרטיס הטיפים שלי @@ -322,9 +326,11 @@ נכשל שאלות נפוצות העברות שבוצעו + קבלת כסף עודד חבר להתחיל להשתמש ב-Code השגת Kin השיגו עוד Kin + תן מזומן העבר קין אין מספיק כספים הזמן חבר @@ -344,6 +350,7 @@ סוגי מטבע בשימוש לאחרונה הפנה חבר, קבל 5$ בונוס הפניה + בקשת כסף בקשת Kin בקשת טיפ נדרש Face ID @@ -358,6 +365,7 @@ הוצאו החלף חשבון תנאי שימוש + כרטיס טיפ מתן Kin כטיפ לא ידוע נדרש עדכון diff --git a/app/src/main/res/values-hi-rIN/strings-localized.xml b/app/src/main/res/values-hi-rIN/strings-localized.xml index fa261100e..684c79da1 100644 --- a/app/src/main/res/values-hi-rIN/strings-localized.xml +++ b/app/src/main/res/values-hi-rIN/strings-localized.xml @@ -1,5 +1,6 @@ + डेबिट कार्ड से कैश जोड़ें कैमरा एक्सेस की अनुमति दें संपर्कों को एक्सेस करने दें पुश नोटिफिकेशन की अनुमति दें @@ -23,6 +24,7 @@ Face ID चालू करें Touch ID चालू करें बाहर निकलें + दें Kin दे आमंत्रित करें आमंत्रण @@ -43,6 +45,7 @@ क्लिपबोर्ड से पेस्ट करें अकाउंट कनेक्ट करने के लिए पोस्ट करें वॉलेट में डालें + पाएँ वर्तमान अकाउंट को पुनर्प्राप्त करें\n याद दिलाएं फ़ोन नंबर हटाएं @@ -53,6 +56,7 @@ सेंड वेरिफिकेशन कोड भेजें शेयर करें + URL के रूप में शेयर करें डाउनलोड लिंक शेयर करें इस वीडियो को शेयर करें मेरा टिप कार्ड दिखाएँ @@ -322,9 +326,11 @@ असफल अक्सर पूछे जाने वाले प्रश्न दिया + कैश पाएं किसी दोस्त से Code इस्तेमाल करना शुरू करवाएं Kin पाएं अधिक Kin पाएं + कैश दें Kin दे अपर्याप्त फंड एक दोस्त को आमंत्रित करें @@ -344,6 +350,7 @@ हाल की मुद्राएं किसी दोस्त को रेफ़र करें, $5 प्राप्त करें रैफरल बोनस + कैश के लिए रिक्वेस्ट करें Kin के लिए अनुरोध करें एक टिप का अनुरोध करें फेस ID की ज़रूरत है @@ -358,6 +365,7 @@ खर्च किया अकाउंट स्विच करें सेवा की शर्तें + टिप कार्ड Kin को टिप दें अज्ञात अपडेट की आवश्यकता है diff --git a/app/src/main/res/values-hu/strings-localized.xml b/app/src/main/res/values-hu/strings-localized.xml index f5307151c..7ae1e607a 100644 --- a/app/src/main/res/values-hu/strings-localized.xml +++ b/app/src/main/res/values-hu/strings-localized.xml @@ -1,5 +1,6 @@ + Adj hozzá készpént egy bankkártyával Kamera-hozzáférés engedélyezése Hozzáférés engedélyezése a névjegyekhez Push értesítések engedélyezése @@ -23,6 +24,7 @@ Arc azonosítás bekapcsolása Érintéses azonosítás bekapcsolása Kilépés + Adj Kin adása Meghív Meghívottak @@ -43,6 +45,7 @@ Beillesztés a vágólapról Írj bejegyzést, hogy csatlakoztasd a fiókodat Walletba helyez + Kapj Meglévő fiók helyreállítása Emlékeztet Telefonszám eltávolítása @@ -53,6 +56,7 @@ Küldés Ellenőrző kód küldése Megosztás + Megosztás URL-ként Letöltési hivatkozás megosztása Ossza meg ezt a videót Borravalós kártyám mutatása @@ -322,9 +326,11 @@ Sikertelen GYIK Adott + Kapj készpénzt Szerezz egy barátot elkezdte használni a Code-ot Szerezz Kint Szerezzen több Kin-t + Adj készpénzt Kin felvétele Nincs elég tőke Hívd meg egy barátod @@ -344,6 +350,7 @@ Legutóbbi pénznemek Ajánlj egy barátot, kapsz 5 dollárt Ajánlói bónusz + Kérj készpénzt Kin kérése Kérj borravalót Face ID-t igényel @@ -358,6 +365,7 @@ Elköltött Fiókcsere Szolgáltatási feltételek + Borravalós kártya Kin borravaló Ismeretlen Frissítés szükséges diff --git a/app/src/main/res/values-id/strings-localized.xml b/app/src/main/res/values-id/strings-localized.xml index 3d4237fb2..8e7f45348 100644 --- a/app/src/main/res/values-id/strings-localized.xml +++ b/app/src/main/res/values-id/strings-localized.xml @@ -1,5 +1,6 @@ + Tambahkan Uang Tunai dengan Kartu Debit Izinkan Akses Kamera Izinkan Akses ke Kontak Izinkan Pemberitahuan Push @@ -23,6 +24,7 @@ Aktifkan Face ID Aktifkan Touch ID Keluar + Berikan Berikan Kin Undang Undangan @@ -43,6 +45,7 @@ Tempel Dari Papan Klip Post untuk Menghubungkan Akun Masukkan ke Dompet + Terima Pulihkan Akun Yang Ada Ingatkan Hapus Nomor Telepon @@ -53,6 +56,7 @@ Kirim Kirim Kode Verifikasi Bagikan + Bagikan sebagai URL Bagikan Tautan Unduhan Bagikan Video Ini Tampilkan Kartu Tip Saya @@ -322,9 +326,11 @@ Gagal Tanya-Jawab Memberi + Dapatkan Uang Tunai Ajak Teman Memulai Code Dapatkan Kin Dapatkan Lebih Banyak Kin + Berikan Uang Berikan Kin Dana Tidak Cukup Undang Teman @@ -344,6 +350,7 @@ Mata Uang Terbaru Ajak Teman, Dapatkan $5 Bonus Referensi + Minta Uang Tunai Minta Kin Minta Tip Memerlukan Face ID @@ -358,6 +365,7 @@ Terpakai Beralih Akun Ketentuan Layanan + Kartu Tip Beri Tip Kin Tak Dikenal Memerlukan Pembaruan diff --git a/app/src/main/res/values-it/strings-localized.xml b/app/src/main/res/values-it/strings-localized.xml index ed6cee185..5d56eaf59 100644 --- a/app/src/main/res/values-it/strings-localized.xml +++ b/app/src/main/res/values-it/strings-localized.xml @@ -1,5 +1,6 @@ + Aggiungi Contanti con una Carta di Debito Consenti l\'accesso alla fotocamera Consenti l\'accesso ai contatti Consenti le notifiche push @@ -23,6 +24,7 @@ Abilita Face ID Abilita Touch ID Esci + Dare Dona Kin Invita Inviti @@ -43,6 +45,7 @@ Incolla dagli appunti Pubblica per Connettere l\'Account Metti nel portafoglio + Ricevere Recupera un account esistente Ricorda Rimuovi numero di telefono @@ -53,6 +56,7 @@ Invia Invia codice di verifica Condividi + Condividi come URL Condividi link per il download Condividi questo video Moatra la mia Tip Card @@ -322,9 +326,11 @@ Non riuscito Domande frequenti Donati + Ottieni Contanti Fai iniziare un amico su Code Ottieni Kin Ottieni più Kin + Dare denaro Dona Kin Fondi insufficienti Invita un amico @@ -344,6 +350,7 @@ Valute recenti Invita un amico, ricevi 5 $ Bonus referral + Richiedi Contanti Richiedi Kin Richiedi una mancia Richiedi Face ID @@ -358,6 +365,7 @@ Spesi Cambia account Termini di servizio + Carta suggerimento Mancia Kin Sconosciuto Aggiornamento richiesto diff --git a/app/src/main/res/values-ja/strings-localized.xml b/app/src/main/res/values-ja/strings-localized.xml index 926f44196..e954c977d 100644 --- a/app/src/main/res/values-ja/strings-localized.xml +++ b/app/src/main/res/values-ja/strings-localized.xml @@ -1,5 +1,6 @@ + デビットカードで現金を追加する カメラへのアクセスを許可する 連絡先へのアクセスを許可する プッシュ通知を許可する @@ -23,6 +24,7 @@ Face IDを有効にする Touch IDを有効にする 終了する + 渡す Kinを与える 招待 招待 @@ -43,6 +45,7 @@ クリップボードから貼り付ける 投稿してアカウントを接続 ウォレットに入れる + 受け取る 既存のアカウントを復元する リマインド 電話番号を削除する @@ -53,6 +56,7 @@ 送信 確認コードを送信する 共有 + URLとして共有する ダウンロードリンクを共有 この動画をシェアする チップカードを表示する @@ -322,9 +326,11 @@ 失敗しました よくある質問 与えた + 現金を得る 友だちにCodeを始めさせる Kinを入手 Kinをもっと入手する + 現金を渡す Kinを与える 資金不足 友達を招待する @@ -344,6 +350,7 @@ 最近の通貨 友だちを紹介して5ドルもらおう 紹介ボーナス + 現金を要求する Kinをリクエスト チップをリクエストする Face IDが必要 @@ -358,6 +365,7 @@ 使用済み アカウントを切り替える 利用規約 + チップカード Kinにチップを支払う 不明 更新が必要です diff --git a/app/src/main/res/values-ko/strings-localized.xml b/app/src/main/res/values-ko/strings-localized.xml index 3ca14b6b0..401affeed 100644 --- a/app/src/main/res/values-ko/strings-localized.xml +++ b/app/src/main/res/values-ko/strings-localized.xml @@ -1,5 +1,6 @@ + 직불 카드로 현금 추가 카메라 접근 허용 연락처 접근 허용 푸쉬 알림 허용 @@ -23,6 +24,7 @@ Face ID 활성화 Touch ID 활성화 나가기 + 주기 Kin 보내기 초대 초대 @@ -43,6 +45,7 @@ 클립보드에서 붙여넣기 계정을 연결하려면 게시하기 지갑에 넣기 + 받기 기존 계정 복구하기 알림 전화번호 삭제 @@ -53,6 +56,7 @@ 보내기 인증 코드 보내기 공유 + URL로 공유 다운로드 링크 공유 이 동영상 공유 내 팁 카드 표시 @@ -322,9 +326,11 @@ 실패함 자주 묻는 질문 보낸 내역 + 현금 받기 친구가 Code를 시작하게 만들기 Kin 받기 킨 더 확보하기 + 현금 주기 Kin 보내기 자금 부족 친구 초대하기 @@ -344,6 +350,7 @@ 최근 통화 친구 추천하고 $5 받으세요 추천 보너스 + 현금 요청 킨 요청 팁 요청 Face ID 필요 @@ -358,6 +365,7 @@ 지출 계정 바꾸기 서비스 약관 + 팁 카드 팁 킨 알 수 없음 업데이트 요구됨 diff --git a/app/src/main/res/values-ms/strings-localized.xml b/app/src/main/res/values-ms/strings-localized.xml index 2f9cdb93d..462978cad 100644 --- a/app/src/main/res/values-ms/strings-localized.xml +++ b/app/src/main/res/values-ms/strings-localized.xml @@ -1,5 +1,6 @@ + Tambah Wang Tunai dengan Kad Debit Benarkan Akses Kamera Benarkan Akses kepada Kenalan Benarkan Notifikasi Tolak @@ -23,6 +24,7 @@ Mengaktifkan ID Wajah Mengaktifkan ID Sentuhan Keluar + Beri Beri Kin Ajak Ajakan @@ -43,6 +45,7 @@ Tampal Dari Papan Keratan Siar untuk Menghubungkan Akaun Masukkan dalam Dompet + Terima Pulihkan Akaun Sedia Ada Ingatkan Singkirkan Nombor Telefon @@ -53,6 +56,7 @@ Hantar Hantar Kod Pengesahan Kongsi + Kongsikan sebagai URL Kongsi Pautan Muat Turun Kongsi Video Ini Tunjuk Kad Tip Saya @@ -322,9 +326,11 @@ Gagal FAQ Beri + Dapatkan Wang Tunai Dapatkan Rakan untuk Bermula menggunakan Code Dapatkan Kin Dapatkan Lebih Banyak Kin + Beri Wang Beri Kin Dana Tidak Mencukupi Ajak Kawan @@ -344,6 +350,7 @@ Mata Wang Baru Rujuk Rakan, Dapat $5 Bonus Rujukan + Minta Wang Tunai Minta Kin Minta Tip Perlukan ID Muka @@ -358,6 +365,7 @@ Perbelanjaan Tukar Akaun Syarat Perkhidmatan + Kad Petua Tip Kin Tidak diketahui Kemas Kini Diperlukan diff --git a/app/src/main/res/values-nl/strings-localized.xml b/app/src/main/res/values-nl/strings-localized.xml index e5a9729ba..d9422b44e 100644 --- a/app/src/main/res/values-nl/strings-localized.xml +++ b/app/src/main/res/values-nl/strings-localized.xml @@ -1,5 +1,6 @@ + Voeg contant geld toe met een bankpas Toegang tot camera toestaan Toegang tot contacten toestaan Pushmeldingen toestaan @@ -23,6 +24,7 @@ Gezichts-ID inschakelen Touch-ID inschakelen Afsluiten + Geven KIN schenken Uitnodigen Uitnodigingen @@ -43,6 +45,7 @@ Plakken van klembord Post om het account te verbinden In wallet plaatsen + Ontvangen Bestaand account herstellen Herinneren Telefoonnummer verwijderen @@ -53,6 +56,7 @@ Verzenden Verificatiecode sturen Delen + Delen als URL Downloadlink delen Deel deze video Toon mijn fooikaart @@ -322,9 +326,11 @@ Mislukt Veelgestelde Vragen Geschonken + Ontvang contant geld Laat een vriend beginnen met Code Krijg Kin Verkrijg meer Kin + Contant geven KIN schenken Onvoldoende fondsen Een vriend uitnodigen @@ -344,6 +350,7 @@ Recente valuta Verwijs een vriend en ontvang $5 Verwijzingsbonus + Vraag contant geld aan Kin aanvragen Een fooi aanvragen Vereist gezichts-ID @@ -358,6 +365,7 @@ Besteed Wisselen van account Servicevoorwaarden + Fooikaart Fooi-kin Onbekend Update vereist diff --git a/app/src/main/res/values-no/strings-localized.xml b/app/src/main/res/values-no/strings-localized.xml index 2517b2a28..b3ba5d8ac 100644 --- a/app/src/main/res/values-no/strings-localized.xml +++ b/app/src/main/res/values-no/strings-localized.xml @@ -1,5 +1,6 @@ + Sett inn penger med et debetkort Tillat kameratilgang Gi tilgang til kontakter Tillat push-varsler @@ -23,6 +24,7 @@ Aktiver Face ID Aktiver Touch ID Avslutt + Gi Gi Kin Inviter Invitasjoner @@ -43,6 +45,7 @@ Lim inn fra utklippstavlen Skriv et innlegg for å koble til kontoen Legg i lommeboken + Motta Gjenopprett eksisterende konto Påminn Fjern telefonnummer @@ -53,6 +56,7 @@ Send Send bekreftelseskode Del + Del som nettadresse Del nedlastingslenke Del denne videoen Vis tipskortet mitt @@ -322,9 +326,11 @@ Mislyktes Vanlige spørsmål Ga + Få penger Få en venn i gang med Code Skaff deg Kin Få flere Kin + Gi kontanter Gi Kin Utilstrekkelige midler Inviter en venn @@ -344,6 +350,7 @@ Nylige valutaer Henvis en venn, få USD 5 Henvisningsbonus + Be om penger Be om Kin Be om tips Krev ansikts-ID @@ -358,6 +365,7 @@ Brukt Bytt konto Brukervilkår + Tipskort Tips Kin Ukjent Oppdatering kreves diff --git a/app/src/main/res/values-pl/strings-localized.xml b/app/src/main/res/values-pl/strings-localized.xml index d04d92e1c..c9f26eeb0 100644 --- a/app/src/main/res/values-pl/strings-localized.xml +++ b/app/src/main/res/values-pl/strings-localized.xml @@ -1,5 +1,6 @@ + Dodaj Cash z użyciem karty debetowej Zezwól na dostęp do kamery Zezwól na dostęp do kontaktów Zezwól na powiadomienia typu push @@ -23,6 +24,7 @@ Włącz funkcję Face ID Włącz funkcję Touch ID Wyjdź + Podaruj Daj Kin Zaproś Zaproszenia @@ -43,6 +45,7 @@ Wklej ze schowka Wyślij post, aby podłączyć konto Umieść w portfelu + Otrzymaj Odzyskaj istniejące konto Przypomnij Usuń numer telefonu @@ -53,6 +56,7 @@ Wyślij Wyślij kod weryfikacyjny Udostępnij + Udostępnij URL Udostępnij odnośnik do pobrania Udostępnij ten film Pokaż moją kartę napiwków @@ -322,9 +326,11 @@ NIe powiodło się Często zadawane pytania Dał + Otrzymaj Cash Poleć znajomemu aplikację Code Uzyskaj kin Zdobądź więcej Kin + Podaruj gotówkę Daj Kin Niewystarczające środki Zaproś znajomego @@ -344,6 +350,7 @@ Niedawne waluty Poleć znajomemu i otrzymaj 5 $ Bonus za polecenie + Poproś o Cash Poproś o Kin Poproś o napiwek Wymagaj Face ID @@ -358,6 +365,7 @@ Wydano Przełącz konta Warunki świadczenia usług + Karta napiwkowa Napiwek Kin Nieznane Wymagana aktualizacja diff --git a/app/src/main/res/values-pt/strings-localized.xml b/app/src/main/res/values-pt/strings-localized.xml index ff81a7648..b36c448fd 100644 --- a/app/src/main/res/values-pt/strings-localized.xml +++ b/app/src/main/res/values-pt/strings-localized.xml @@ -1,5 +1,6 @@ + Adicionar dinheiro com um cartão de débito Permitir Acesso à Câmara Permitir Acesso aos Contactos Permitir Notificações Push @@ -23,6 +24,7 @@ Ativar a identificação facial Ativar a identificação por toque Sair + Dar Dar Kin Convidar Convites @@ -43,6 +45,7 @@ Colar a Partir da Área de Transferência Faça uma publicação para associar conta Por na Carteira + Receber Recuperar Conta Existente Relembrar Remover Número de Telefone @@ -53,6 +56,7 @@ Enviar Enviar Código de Verificação Partilhar + Partilhar como URL Partilhar Ligação de Download Partilhar este vídeo Exibir o meu Cartão de Recompensas @@ -322,9 +326,11 @@ Falhou Guiar FAQ Deu + Receber dinheiro Iniciar um Amigo na Code Obter Kin Obter mais Kin + Dar dinheiro Dar Kin Fundos insuficientes Convidar um Amigo @@ -344,6 +350,7 @@ Moedas Recentes Refira a uma amigo, ganhe 5€ Bónus de Referência + Solicitar dinheiro Solicitar Kin Solicitar uma recompensa Pedir Face ID @@ -358,6 +365,7 @@ Gastou Mudar de Conta Termos de Serviço + Cartão de Dica Dar recompensa em Kin Desconhecido Atualização Necessária diff --git a/app/src/main/res/values-ro/strings-localized.xml b/app/src/main/res/values-ro/strings-localized.xml index 978700d63..31da11d35 100644 --- a/app/src/main/res/values-ro/strings-localized.xml +++ b/app/src/main/res/values-ro/strings-localized.xml @@ -1,5 +1,6 @@ + Adăugați numerar cu un card de debit Permite accesul la fotocameră Permite accesul la contacte Permite notificările push @@ -23,6 +24,7 @@ Activează Face ID Activează Touch ID Ieșire + Trimite Dă Kin Invită Invitații @@ -43,6 +45,7 @@ Lipește din clipboard Postează pentru a conecta contul Pune în portofel + Primește Recuperează un cont existent Reamintește Elimină numărul de telefon @@ -53,6 +56,7 @@ Trimite Trimite verificarea pentru Code Distribuie + Distribuie ca adresă URL Partajați link-ul de descărcare Partajează acest videoclip Arată cardul meu de bacșiș @@ -322,9 +326,11 @@ Eșuat Întrebări frecvente Trimis + Obțineți numerar Ajutați un prieten să înceapă cu Code Obține kin Obține mai mulţi Kin + Trimite cash Trimite Kin Fonduri insuficiente Invită un prieten @@ -344,6 +350,7 @@ Valute folosite recent Recomandați un prieten, primiți 5 dolari Bonus de recomandare + Solicitați numerar Solicită monede Kin Solicită un bacșiș Solicită Face ID @@ -358,6 +365,7 @@ Cheltuiți Folosește alt cont Termeni de utilizare + Card de bacșiș Trimite bacșiș monede Kin Necunoscut Este necesară actualizarea diff --git a/app/src/main/res/values-ru/strings-localized.xml b/app/src/main/res/values-ru/strings-localized.xml index bcd48da52..e1fdf7474 100644 --- a/app/src/main/res/values-ru/strings-localized.xml +++ b/app/src/main/res/values-ru/strings-localized.xml @@ -1,5 +1,6 @@ + Добавить наличные с помощью дебетовой карты Разрешить доступ к камере Разрешить доступ к контактам Разрешить push-уведомления @@ -23,6 +24,7 @@ Включить Face ID Включить Touch ID Выйти + Дать Дать Kin Пригласить Приглашения @@ -43,6 +45,7 @@ Вставить из буфера обмена Опубликуйте пост, чтобы подключить аккаунт Положить в кошелек + Получить Восстановить существующую учетную запись Напомнить Удалить номер телефона @@ -53,6 +56,7 @@ Отправить Отправить код проверки Поделиться + Поделиться в виде URL Поделитесь ссылкой для загрузки Поделитесь этим видео Показать мою карту чаевых @@ -322,9 +326,11 @@ Сбой Частые вопросы Дал(-а) + Получить наличные Пригласите друга начать пользоваться Code Получить Kin Получить больше Kin + Дать деньги Дал(-а) Kin Недостаточно средств Пригласить друга @@ -344,6 +350,7 @@ Недавние валюты Приведите друга, получите 5 долл. США Реферальный бонус + Потребовать наличные Запросить кин Запросить чаевые Запрашивать Face ID @@ -358,6 +365,7 @@ Потрачено Переключить учетные записи Условия пользования + Карточка чаевых Дать чаевые в кин Неизвестный Требуется обновление diff --git a/app/src/main/res/values-sk/strings-localized.xml b/app/src/main/res/values-sk/strings-localized.xml index 859e5883f..bc89b18a4 100644 --- a/app/src/main/res/values-sk/strings-localized.xml +++ b/app/src/main/res/values-sk/strings-localized.xml @@ -1,5 +1,6 @@ + Navýšiť hotovosť z debetnej karty Povoliť prístup k fotoaparátu Povoliť prístup ku kontaktom Povoliť upozornenia push @@ -23,6 +24,7 @@ Povoliť Face ID Povoliť Touch ID Odísť + Dať Dať Kin Pozvať Pozýva @@ -43,6 +45,7 @@ Prilepiť zo schránky Uverejniť pre prepojenie účtu Vložiť do peňaženky + Dostať Obnoviť existujúci účet Pripomenúť Odstrániť telefónne číslo @@ -53,6 +56,7 @@ Odoslať Poslať overovací kód Zdieľať + Zdieľať ako adresu URL Zdieľať odkaz na stiahnutie Zdieľajte toto video Zobraziť moju tipovaciu kartu @@ -322,9 +326,11 @@ Zlyhalo Často kladené otázky Dal + Získať hotovosť Začnite používať kód priateľa Získajte Kin Získajte viac Kinov + Dať hotovosť Dať Kin Nedostatok prostriedkov Pozvať priateľa @@ -344,6 +350,7 @@ Najnovšie meny Odporučte priateľa a získajte 5 dolárov Bonus za odporúčanie + Požiadať o hotovosť Požiadať o kin Požiadať o tip Vyžaduje Face ID @@ -358,6 +365,7 @@ Použité Prepnúť účty Podmienky služby + Karta tringeltov Tipovať kin Neznámy Vyžaduje sa aktualizácia diff --git a/app/src/main/res/values-sr/strings-localized.xml b/app/src/main/res/values-sr/strings-localized.xml index 99dc958fc..7e134fbc1 100644 --- a/app/src/main/res/values-sr/strings-localized.xml +++ b/app/src/main/res/values-sr/strings-localized.xml @@ -1,5 +1,6 @@ + Додај готовину помоћу дебитне картице Dozvoli pristup kameri Dozvoli pristup kontaktima Dozvoli prosleđena obaveštenja @@ -23,6 +24,7 @@ Омогући Face ID Омогући Touch ID Izađi + Дај Daj Kin Pozovi Pozivi @@ -43,6 +45,7 @@ Nalepi iz ostave Поставите објаву да повежете налог Stavi u novčanik + Прими Oporavi postojeći nalog Podseti Ukloni broj telefona @@ -53,6 +56,7 @@ Пошаљи Pošalji verifikacioni kod Подели + Дели као URL адресу Подели везу за преузимање Дели овај видео-запис Покажи моју картицу за напојнице @@ -322,9 +326,11 @@ Nije uspelo Najčešća pitanja Dato + Узми готовину Нека пријатељ започне са Кодом Прибави Кин Набавите још кинова + Давање готовине Daj Kin Нема довољно средстава Pozovi prijatelja @@ -344,6 +350,7 @@ Nedavne valute Препоручите пријатеља, узмите 5$ Бонус за препоруке + Захтевај готовину Затражи Кин Затражи напојницу Захтевање Face ID-ја @@ -358,6 +365,7 @@ Потрошено Promeni naloge Uslovi korišcenja usluge + Картица са саветом Дај напојницу у виду Кина Непознато Potrebno je ažuriranje diff --git a/app/src/main/res/values-sv/strings-localized.xml b/app/src/main/res/values-sv/strings-localized.xml index 6c1809cda..714330066 100644 --- a/app/src/main/res/values-sv/strings-localized.xml +++ b/app/src/main/res/values-sv/strings-localized.xml @@ -1,5 +1,6 @@ + Lägg till kontanter med ett betalkort Tillåt åtkomst till kameran Tillåt åtkomst till kontakter Tillåt push-notiser @@ -23,6 +24,7 @@ Aktivera Face ID Aktivera Touch ID Avsluta + Ge Ge Kin Bjud in Bjuder in @@ -43,6 +45,7 @@ Klistra in från Urklipp Posta för att koppla kontot Lägg i plånboken + Ta emot Återskapa befintligt konto Påminn Ta bort telefonnummer @@ -53,6 +56,7 @@ Skicka Skicka verifieringskod Dela + Dela som webbadress Dela nedladdningslänk Dela den här videon Visa mitt drickskort @@ -322,9 +326,11 @@ Misslyckad VANLIGA FRÅGOR Gav + Få kontanter Få en vän att börja med Code Få Kin Skaffa mer Kin + Ge pengar Ge Kin Otillräckliga medel Bjud in en vän @@ -344,6 +350,7 @@ Senaste valutorna Värva en vän, få 5 USD Värvningsbonus + Begär kontanter Begär Kin Be om dricks Kräv Face ID @@ -358,6 +365,7 @@ Spenderat Byt konto Villkor för tjänsten + Drickskort Ge Kin i dricks Okänt Uppdatering krävs diff --git a/app/src/main/res/values-th/strings-localized.xml b/app/src/main/res/values-th/strings-localized.xml index 98c4f7091..e3647e40d 100644 --- a/app/src/main/res/values-th/strings-localized.xml +++ b/app/src/main/res/values-th/strings-localized.xml @@ -1,5 +1,6 @@ + เติมเงินสดด้วยบัตรเดบิต อนุญาตให้เข้าถึงกล้อง อนุญาตให้เข้าถึงรายชื่อติดต่อ อนุญาตให้แจ้งเตือนแบบพุช @@ -23,6 +24,7 @@ เปิดใช้งาน Face ID เปิดใช้งาน Touch ID ออก + ให้ ให้ Kin เชิญ เชิญ @@ -43,6 +45,7 @@ วางจากคลิปบอร์ด โพสต์เพื่อเชื่อมโยงบัญชี\n ใส่ในกระเป๋าเงิน + รับ กู้คืนบัญชีที่มีอยู่ เตือน ลบหมายเลขโทรศัพท์ @@ -53,6 +56,7 @@ ส่ง ส่งรหัสยืนยัน แชร์ + แชร์เป็น URL แชร์ลิงก์ดาวน์โหลด แชร์วิดีโอนี้ แสดงบัตรทิปของฉัน @@ -322,9 +326,11 @@ ล้มเหลว คำถามที่พบบ่อย ให้ + รับเงินสด ชวนเพื่อนมาเริ่มต้นใช้งาน Code รับ Kin รับ Kin เพิ่มเติม + ให้เงินสด ให้ Kin เงินทุนไม่เพียงพอ ชวนเพื่อน @@ -344,6 +350,7 @@ สกุลเงินล่าสุด แนะนำเพื่อน รับ $5 โบนัสการแนะนำ + ขอรับเงินสด ขอรับ Kin ขอรับทิป ต้องใช้ Face ID @@ -358,6 +365,7 @@ ใช้จ่าย สลับบัญชี เงื่อนไขการให้บริการ + บัตรทิป ให้ทิปเป็น Kin ไม่ทราบ จำเป็นต้องอัปเดต diff --git a/app/src/main/res/values-tr/strings-localized.xml b/app/src/main/res/values-tr/strings-localized.xml index 62f3a38d7..5e0677be5 100644 --- a/app/src/main/res/values-tr/strings-localized.xml +++ b/app/src/main/res/values-tr/strings-localized.xml @@ -1,5 +1,6 @@ + Banka Kartıyla Nakit Ekle Kamera Erişimine İzin Ver Kişiler\'e Erişime İzin Ver Anlık Bildirimlere İzin Ver @@ -23,6 +24,7 @@ Face ID\'yi etkinleştir Touch ID\'yi etkinleştir Çıkış + Ver Kin Ver Davet Et Davetler @@ -43,6 +45,7 @@ Panodan Yapıştır Hesabı Bağlamak için Gönder Cüzdana Yerleştir + Al Mevcut Hesabı Kurtar Hatırlat Telefon Numarasını Kaldır @@ -53,6 +56,7 @@ Gönder Doğrulama Kodu Gönder Paylaş + URL olarak paylaş İndirme Bağlantısı Paylaş Bu Videoyu Paylaş Bahşiş Kartımı Göster @@ -322,9 +326,11 @@ Başarısız SSS Verdi + Nakıt Al Bir Arkadaşınızı Code\'a başlatın Kin Al Daha Fazla Kin Al + Nakit Ver Kin Ver Yetersiz Bakiye Arkadaşını Davet Et @@ -344,6 +350,7 @@ Son Para Birimleri Bir Arkadaş Yönlendir, $5\'ı Kazan Yönlendirme Bonusu + Nakit Talep Et Kin Talep Et Bahşiş İste Face ID gerekir @@ -358,6 +365,7 @@ Harcanan Hesap Değiştir Hizmet Şartları + Bahşiş Kartı Bahşiş Olarak Kin Ver Bilinmeyen Güncelleme Gerekli diff --git a/app/src/main/res/values-uk/strings-localized.xml b/app/src/main/res/values-uk/strings-localized.xml index 3caf97073..a31dd498c 100644 --- a/app/src/main/res/values-uk/strings-localized.xml +++ b/app/src/main/res/values-uk/strings-localized.xml @@ -1,5 +1,6 @@ + Внести гроші з дебетової картки Дозволити доступ до камери Дозволити доступ до контактів Дозволити push-сповіщення @@ -23,6 +24,7 @@ Увімкнути Face ID Увімкнути Touch ID Вийти + Дати Дати Кін Запросити Запрошення @@ -43,6 +45,7 @@ Вставити з буфера обміну Зробіть публікацію, щоб прив’язати акаунт Покласти в гаманець + Отримати Відновити існуючий акаунт Нагадати Видалити номер телефону @@ -53,6 +56,7 @@ Надіслати Надіслати код підтвердження Поділитися + Поділитися як URL-адресою Поділитись посиланням на завантаження Поділитися цим відео Показати мою картку для чайових @@ -322,9 +326,11 @@ Не вдалося Часто задавані питання Надано + Отримати гроші. Запросіть друга в Code Отримати Kin Отримати більше Kin + Дати готівку Дати Kin Недостатньо коштів Запросити друга @@ -344,6 +350,7 @@ Останні валюти Порекомендуйте другу, отримайте 5 дол. Реферальний бонус + Запросити грошовий переказ Запит на Kin Запросити чайові Вимагати Face ID @@ -358,6 +365,7 @@ Витрачено Змінити акаунт Умови обслуговування + Картка з підказками Дати чайові у Kin Невідомо Потрібне оновлення diff --git a/app/src/main/res/values-vi/strings-localized.xml b/app/src/main/res/values-vi/strings-localized.xml index 248c92691..016ee9983 100644 --- a/app/src/main/res/values-vi/strings-localized.xml +++ b/app/src/main/res/values-vi/strings-localized.xml @@ -1,5 +1,6 @@ + Thêm Tiền mặt Bằng Thẻ Ghi nợ Cho phép Truy cập Camera Cho phép Truy cập vào Danh bạ Cho phép Thông báo Đẩy @@ -23,6 +24,7 @@ Bật Face ID Bật Touch ID Thoát + Cho Tặng Kin Mời Lời mời @@ -43,6 +45,7 @@ Dán Từ Khay nhớ tạm Hãy đăng bài để kết nối tài khoản Đặt trong Ví + Nhận Khôi phục Tài khoản Hiện có Nhắc nhở Xóa Số Điện thoại @@ -53,6 +56,7 @@ Gửi Gửi Mã Xác minh Chia sẻ + Chia sẻ dưới dạng URL Chia sẻ đường liên kết tải xuống Chia sẻ Video Này Hiển thị Thẻ tiền boa của tôi @@ -322,9 +326,11 @@ Không thành công Hỏi Đáp Đã tặng + Nhận Tiền mặt Mời một người bạn bắt đầu dùng Code Nhận Kin Nhận Thêm Kin + Cho Tiền mặt Tặng Kin Không đủ Tiền Mời Bạn bè @@ -344,6 +350,7 @@ Đơn vị tiền tệ Gần đây Giới thiệu một người bạn - Nhận 5$ Tiền thưởng Giới thiệu + Yêu cầu Tiền mặt Yêu cầu Kin Yêu cầu Tiền boa Cần có Face ID @@ -358,6 +365,7 @@ Đã chi tiêu Chuyển đổi Tài khoản Điều khoản Dịch vụ + Thẻ Boa Boa Kin Không xác định Cần Cập nhật diff --git a/app/src/main/res/values-zh-rCN/strings-localized.xml b/app/src/main/res/values-zh-rCN/strings-localized.xml index 26f5c4c5d..60dfb4f30 100644 --- a/app/src/main/res/values-zh-rCN/strings-localized.xml +++ b/app/src/main/res/values-zh-rCN/strings-localized.xml @@ -1,5 +1,6 @@ + 使用借记卡添加现金 允许相机访问 允许访问通讯录 允许推送通知 @@ -23,6 +24,7 @@ 启用 Face ID 启用 Touch ID 退出 + 给出 给与 Kin 邀请 邀请 @@ -43,6 +45,7 @@ 从剪贴板粘贴 发布至连接的账户 放入钱包 + 接收 恢复现有账户 提醒 移除手机号码 @@ -53,6 +56,7 @@ 发送 发送验证码 分享 + 以 URL 形式分享 分享下载链接 分享此视频 显示我的小费卡 @@ -322,9 +326,11 @@ 失败 常见问题 已给 + 获取现金 让好友开始使用 Code 获取 Kin 币 获取更多 Kin 币 + 给出现金 给与 Kin 资金不足 邀请好友 @@ -344,6 +350,7 @@ 最近的货币 推荐好友,获得 $5 推荐奖金 + 申请现金 请求 Kin 请求小费 需要Face ID @@ -358,6 +365,7 @@ 花费 切换账户 服务条款 + 小费卡 Kin 小费 未知 需要更新 diff --git a/app/src/main/res/values-zh-rTW/strings-localized.xml b/app/src/main/res/values-zh-rTW/strings-localized.xml index 0fe3c7b48..f5c77f818 100644 --- a/app/src/main/res/values-zh-rTW/strings-localized.xml +++ b/app/src/main/res/values-zh-rTW/strings-localized.xml @@ -1,5 +1,6 @@ + 使用簽帳金融卡添加現金 允許相機存取權 允許存取聯絡人 允許推播通知 @@ -23,6 +24,7 @@ 啟用 Face ID 啟用 Face ID 退出 + 給予 給予 Kin 邀請 邀請 @@ -43,6 +45,7 @@ 從剪貼簿貼上 發文以連結帳戶 放入錢包 + 接受 恢復現有帳戶 提醒 移除電話號碼 @@ -53,6 +56,7 @@ 發送 傳送驗證碼 分享 + 分享網址連結 分享下載連結 分享該影片 顯示我的 Tip Card @@ -322,9 +326,11 @@ 失敗 常見問題集 已給予 + 獲取現金 邀請朋友開始使用 Code 錢包 取得 Kin 獲得更多 Kin + 給予現金 給予 Kin 餘額不足 邀請朋友 @@ -344,6 +350,7 @@ 最近使用的貨幣 推薦一位朋友,可獲 5 美元獎勵 推薦獎勵 + 請求現金 要求 Kin 要求小費 需要 Face ID @@ -358,6 +365,7 @@ 花費 切換帳戶 服務條款 + 提示卡 以 Kin 給小費 不明 需要更新 diff --git a/app/src/main/res/values/strings-localized.xml b/app/src/main/res/values/strings-localized.xml index 722631b40..f7041861d 100644 --- a/app/src/main/res/values/strings-localized.xml +++ b/app/src/main/res/values/strings-localized.xml @@ -1,11 +1,12 @@ + Add Cash with a Debit Card Allow Camera Access Allow Access to Contacts Allow Push Notifications Balance - Add Cash with a Debit Card - Add Cash with a Debit Card + Buy Kin + Buy More Kin Cancel Cancel Send Collect This Cash @@ -23,7 +24,8 @@ Enable Face ID Enable Touch ID Exit - Give Cash + Give + Give Kin Invite Invites Join the Waitlist @@ -43,6 +45,7 @@ Paste From Clipboard Post to Connect Account Put in Wallet + Receive Recover Existing Account Remind Remove Phone Number @@ -53,6 +56,7 @@ Send Send Verification Code Share + Share as a URL Share Download Link Share This Video Show My Tip Card @@ -68,10 +72,10 @@ Unsubscribe Update View Access Key - Withdraw Cash + Withdraw Kin Wrote the 12 Words Down Instead? Yes - Yes, Withdraw Cash + Yes, Withdraw Kin Yes, I Wrote Them Down and Kin @@ -159,7 +163,7 @@ We believe payments should be simple, powerful, and global. By building with advanced blockchain technology Code offers features that traditional payments apps can\'t, like global peer to peer transfers, micropayments that unlock individual articles online, and zero fee tips for your favorite creators. Kin is a cryptocurrency like Bitcoin, but is also designed for fast, inexpensive payments. Like Bitcoin there is only a limited amount of Kin available. If more people buy Kin the value goes up, and if more people sell Kin the value goes down. This dynamic allows everyone who holds Kin to share in the value creation if adoption of Kin grows. - You can buy Kin with your debit card. This is accessible in the Get Kin tab. + You can buy Kin with your debit card. This is accessible in the Get Cash tab. Yes you can. Selling Kin is supported on a number of cryptocurrency exchanges. There are three main ways you can help: talk about your Code experience on social media, encourage your friends to try out Code for themselves, and encourage your favorite websites to integrate Code payments by asking them to check out [getcode.com](https://getcode.com). What is Code? @@ -203,7 +207,7 @@ You received %1$s of Kin for sending someone their first Kin. You can now request tips. Deposit Received - Cash Returned + Kin Returned New on Code Referral Bonus Received X Account Connected @@ -215,7 +219,7 @@ Code enables you to receive Kin by pointing your camera at the digital bill on another user\'s phone You need to turn on Camera in Settings to scan Codes Authenticate to access Code. - Add Cash with a Debit Card + Buy Kin Buy Kin (Coming Soon) Buying and selling Kin is currently a complex process. These processes will get simpler over time. If you want to learn how to buy and sell Kin today you can watch the walk through videos below. You can only give up to %1$s @@ -231,10 +235,10 @@ Make sure you have your Secret Recovery Phrase saved, and then enter “Delete” to delete your Code account. This action is irreversible. Didn\'t get an SMS at %1$s? Didn\'t get an SMS? Resend - Disabling Face ID requires you to verify your identity. + Disabling Face ID requires you to authenticate. Don\'t have the Code Wallet app? You don\'t have any Kin yet. - Enable Face ID to further enhance the security of transaction in Code. + Enable Face ID to require authentication when opening Code. Enter destination address Enter up to %1$s Get $5 of Kin for free when you get a friend to sign up for Code and you send them their first Kin. @@ -322,10 +326,12 @@ Failed FAQ Gave + Get Cash Get a Friend Started on Code - Get Cash - Get More Cash - Give Cash + Get Kin + Get More Kin + Give Cash + Give Kin Insufficient Funds Invite a Friend Limited Time Offer @@ -344,6 +350,7 @@ Recent Currencies Refer a Friend, Get $5 Referral Bonus + Request Cash Request Kin Request a Tip Require Face ID @@ -358,13 +365,14 @@ Spent Switch Accounts Terms of Service - Tip Cash + Tip Card + Tip Kin Unknown Update Required Verify Phone Number Welcome Bonus - Withdraw Cash + Withdraw Kin Withdrew Your Access Key - Tap the logo to share the app download link + Tap the Code logo to share the app download link diff --git a/app/src/main/res/values/strings-universal.xml b/app/src/main/res/values/strings-universal.xml index a04e8d6a5..9cbb2942c 100644 --- a/app/src/main/res/values/strings-universal.xml +++ b/app/src/main/res/values/strings-universal.xml @@ -20,10 +20,12 @@ Vibrate on Scan Show Connectivity Status Tip Card - Tip Chats - Tip Chats Cash + Tip Card on Home Screen + Chats + Cash Transfers in Chat Currency Selection in Balance Buy Kin Internally + Share Tweets to Tip Show Errors If enabled, you\'ll gain the ability to tap the balance on the Balance screen to inspect individual bucket balances. If enabled, a \"No Connection\" badge will show on the scan screen when no internet is detected. @@ -34,9 +36,11 @@ If enabled, a relationship account will be established with getcode.com if it doesn\'t yet exist. If enabled, an option to unsubscribe from a chat will appear for supported chats. If enabled, you\'ll gain the ability to share a tip card. - If enabled, you\'ll gain the ability to chat with tippers. - If enabled, you\'ll gain the ability to send Kin in Tip Chats. + If enabled, your tip card will replace Get Cash on the home screen. + If enabled, you\'ll gain the ability to chat with with other code users. + If enabled, you\'ll gain the ability to send cash in conversations. If enabled, the Buy Kin flow will open in an internal WebView. + If enabled, you\'ll gain the ability to share tweets directly from Twitter to Code to tip the author. %1$s %2$s Reset Tooltips diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a52bd9cbd..192f7066e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,4 +49,7 @@ Connect Account Connecting your X account allows you to reveal your identity to the people you tip. To connect your account post to X. Use Kado Sandbox + + Chat + Chat diff --git a/build.gradle.kts b/build.gradle.kts index 8570b51bd..96e44c946 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ buildscript { repositories { + gradlePluginPortal() google() mavenCentral() maven(url = "https://plugins.gradle.org/m2/") @@ -18,6 +19,7 @@ buildscript { classpath(Classpath.secrets_gradle_plugin) classpath(Classpath.kotlin_serialization_plugin) classpath(Classpath.protobuf_plugin) + classpath(Classpath.versioning_gradle_plugin) } } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 65c1b8d78..6d163128a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -12,10 +12,8 @@ object Android { object Packaging { private const val majorVersion = 2 private const val minorVersion = 1 - private const val patchVersion = 7 - private const val buildNumber = 412 + private const val patchVersion = 8 - const val versionCode = buildNumber const val versionName = "$majorVersion.$minorVersion.$patchVersion" } @@ -41,6 +39,7 @@ object Versions { const val sqlcipher = "4.5.1@aar" const val compose = "2024.05.00" + // compose compiler is tied to [Versions.kotlin] // See compatibility mapping here: // https://developer.android.com/jetpack/androidx/releases/compose-compiler @@ -68,8 +67,10 @@ object Versions { const val play_service_auth = "20.7.0" const val play_service_auth_phone = "18.0.2" + const val grpc: String = "1.62.2" const val grpc_okhttp: String = "1.33.1" - const val grpc_kotlin: String = "1.0.0" + const val grpc_kotlin: String = "1.4.1" + const val protobuf: String = "3.25.3" const val mp_android_chart: String = "v3.1.0" const val lib_phone_number_port: String = "8.12.43" @@ -86,24 +87,31 @@ object Versions { const val markwon = "4.6.2" const val timber = "5.0.1" const val voyager = "1.0.0" - const val protobuf_plugin = "0.8.14" + const val protobuf_plugin = "0.9.4" const val sodium_bindings = "0.9.0" } object Classpath { - const val android_gradle_build_tools = "com.android.tools.build:gradle:${Versions.android_gradle_build_tools}" + const val android_gradle_build_tools = + "com.android.tools.build:gradle:${Versions.android_gradle_build_tools}" const val kotlin_hilt_plugin = "com.google.dagger:hilt-android-gradle-plugin:${Versions.hilt}" - const val androidx_navigation_safeargs = "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}" + const val androidx_navigation_safeargs = + "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_navigation}" const val kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" - const val kotlin_serialization_plugin = "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}" + const val kotlin_serialization_plugin = + "org.jetbrains.kotlin:kotlin-serialization:${Versions.kotlin}" const val google_services = "com.google.gms:google-services:${Versions.google_services}" - const val protobuf_plugin = "com.google.protobuf:protobuf-gradle-plugin:${Versions.protobuf_plugin}" + const val protobuf_plugin = + "com.google.protobuf:protobuf-gradle-plugin:${Versions.protobuf_plugin}" - const val crashlytics_gradle = "com.google.firebase:firebase-crashlytics-gradle:${Versions.crashlytics_gradle}" + const val crashlytics_gradle = + "com.google.firebase:firebase-crashlytics-gradle:${Versions.crashlytics_gradle}" const val bugsnag = "com.bugsnag:bugsnag-android-gradle-plugin:8.+" const val firebase_perf = "com.google.firebase:perf-plugin:1.4.2" - const val secrets_gradle_plugin = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1" + const val secrets_gradle_plugin = + "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1" + const val versioning_gradle_plugin = "de.nanogiants:android-versioning:2.4.0" } object Plugins { @@ -119,7 +127,9 @@ object Plugins { const val firebase_crashlytics = "com.google.firebase.crashlytics" const val firebase_perf = "com.google.firebase.firebase-perf" const val bugsnag = "com.bugsnag.android.gradle" - const val secrets_gradle_plugin = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" + const val secrets_gradle_plugin = + "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" + const val versioning_gradle_plugin = "de.nanogiants.android-versioning" } object Libs { @@ -128,9 +138,11 @@ object Libs { const val androidx_annotation = "androidx.annotation:annotation:${Versions.androidx_annotation}" const val androidx_biometrics = "androidx.biometric:biometric:${Versions.androidx_biometrics}" const val androidx_camerax_core = "androidx.camera:camera-core:${Versions.androidx_camerax}" - const val androidx_camerax_camera2 = "androidx.camera:camera-camera2:${Versions.androidx_camerax}" - const val androidx_camerax_lifecycle = "androidx.camera:camera-lifecycle:${Versions.androidx_camerax}" - const val androidx_camerax_view = "androidx.camera:camera-view:${Versions.androidx_camerax}" + const val androidx_camerax_camera2 = + "androidx.camera:camera-camera2:${Versions.androidx_camerax}" + const val androidx_camerax_lifecycle = + "androidx.camera:camera-lifecycle:${Versions.androidx_camerax}" + const val androidx_camerax_view = "androidx.camera:camera-view:${Versions.androidx_camerax}" const val androidx_core = "androidx.core:core-ktx:${Versions.androidx_core}" const val androidx_constraint_layout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraint_layout}" @@ -141,7 +153,8 @@ object Libs { const val androidx_navigation_ui = "androidx.navigation:navigation-ui-ktx:${Versions.androidx_navigation}" const val androidx_browser = "androidx.browser:browser:${Versions.androidx_browser}" - const val androidx_paging_runtime = "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}" + const val androidx_paging_runtime = + "androidx.paging:paging-runtime-ktx:${Versions.androidx_paging}" const val androidx_room_runtime = "androidx.room:room-runtime:${Versions.androidx_room}" const val androidx_room_rxjava3 = "androidx.room:room-rxjava3:${Versions.androidx_room}" @@ -165,10 +178,14 @@ object Libs { "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:${Versions.kotlinx_coroutines}" const val kotlinx_coroutines_test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinx_coroutines}" - const val kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6" - const val kotlinx_datetime = "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.kotlinx_datetime}" - const val kotlinx_serialization_core = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinx_serialization}" - const val kotlinx_serialization_json = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinx_serialization}" + const val kotlinx_collections_immutable = + "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6" + const val kotlinx_datetime = + "org.jetbrains.kotlinx:kotlinx-datetime:${Versions.kotlinx_datetime}" + const val kotlinx_serialization_core = + "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinx_serialization}" + const val kotlinx_serialization_json = + "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinx_serialization}" const val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}" const val okhttp_logging_interceptor = @@ -177,7 +194,8 @@ object Libs { const val androidx_datastore = "androidx.datastore:datastore-preferences:1.1.1" const val androidx_constraint_layout_compose = "androidx.constraintlayout:constraintlayout-compose:1.0.1" - const val androidx_lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" + const val androidx_lifecycle_viewmodel = + "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.androidx_lifecycle}" const val compose_bom = "androidx.compose:compose-bom:${Versions.compose}" const val compose_accompanist = @@ -190,7 +208,8 @@ object Libs { "androidx.compose.ui:ui-tooling-preview" const val compose_foundation = "androidx.compose.foundation:foundation" const val compose_material = "androidx.compose.material:material" - const val compose_materialIconsExtended = "androidx.compose.material:material-icons-extended-android" + const val compose_materialIconsExtended = + "androidx.compose.material:material-icons-extended-android" const val compose_activities = "androidx.activity:activity-compose:${Versions.compose_activities}" const val compose_view_models = @@ -199,10 +218,14 @@ object Libs { const val compose_navigation = "androidx.navigation:navigation-compose:${Versions.compose_navigation}" const val compose_paging = "androidx.paging:paging-compose:${Versions.compose_paging}" - const val compose_voyager_navigation = "cafe.adriel.voyager:voyager-navigator:${Versions.voyager}" - const val compose_voyager_navigation_hilt = "cafe.adriel.voyager:voyager-hilt:${Versions.voyager}" - const val compose_voyager_navigation_bottomsheet = "cafe.adriel.voyager:voyager-bottom-sheet-navigator:${Versions.voyager}" - const val compose_voyager_navigation_transitions = "cafe.adriel.voyager:voyager-transitions:${Versions.voyager}" + const val compose_voyager_navigation = + "cafe.adriel.voyager:voyager-navigator:${Versions.voyager}" + const val compose_voyager_navigation_hilt = + "cafe.adriel.voyager:voyager-hilt:${Versions.voyager}" + const val compose_voyager_navigation_bottomsheet = + "cafe.adriel.voyager:voyager-bottom-sheet-navigator:${Versions.voyager}" + const val compose_voyager_navigation_transitions = + "cafe.adriel.voyager:voyager-transitions:${Versions.voyager}" const val compose_webview = "io.github.kevinnzou:compose-webview:${Versions.compose_webview}" const val rxjava = "io.reactivex.rxjava3:rxjava:${Versions.rxjava}" @@ -220,17 +243,26 @@ object Libs { const val firebase_perf = "com.google.firebase:firebase-perf" const val play_integrity = "com.google.android.play:integrity:1.3.0" - const val play_service_auth = "com.google.android.gms:play-services-auth:${Versions.play_service_auth}" - const val play_service_auth_phone = "com.google.android.gms:play-services-auth-api-phone:${Versions.play_service_auth_phone}" + const val play_service_auth = + "com.google.android.gms:play-services-auth:${Versions.play_service_auth}" + const val play_service_auth_phone = + "com.google.android.gms:play-services-auth-api-phone:${Versions.play_service_auth_phone}" const val grpc_okhttp = "io.grpc:grpc-okhttp:${Versions.grpc_okhttp}" - const val grpc_kotlin = "io.grpc:grpc-kotlin-stub-lite:${Versions.grpc_kotlin}" + const val grpc_kotlin = "io.grpc:grpc-kotlin-stub:${Versions.grpc_kotlin}" + const val grpc_protobuf = "io.grpc:grpc-protobuf:${Versions.grpc}" + const val grpc_protobuf_lite = "io.grpc:grpc-protobuf-lite:${Versions.grpc}" + const val grpc_stub = "io.grpc:grpc-stub:${Versions.grpc}" + const val protobuf_java = "com.google.protobuf:protobuf-java:${Versions.protobuf}" + const val protobuf_kotlin_lite = "com.google.protobuf:protobuf-kotlin-lite:${Versions.protobuf}" const val inject = "javax.inject:javax.inject:1" const val mp_android_chart = "com.github.PhilJay:MPAndroidChart:${Versions.mp_android_chart}" - const val lib_phone_number_port = "io.michaelrocks:libphonenumber-android:${Versions.lib_phone_number_port}" - const val lib_phone_number_google = "com.googlecode.libphonenumber:libphonenumber:${Versions.lib_phone_number_google}" + const val lib_phone_number_port = + "io.michaelrocks:libphonenumber-android:${Versions.lib_phone_number_port}" + const val lib_phone_number_google = + "com.googlecode.libphonenumber:libphonenumber:${Versions.lib_phone_number_google}" const val hilt_nav_compose = "androidx.hilt:hilt-navigation-compose:1.1.0-alpha01" const val zxing = "com.google.zxing:core:${Versions.zxing}" @@ -257,7 +289,11 @@ object Libs { const val cloudy = "com.github.skydoves:cloudy:0.1.2" - const val sodium_bindings = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android:${Versions.sodium_bindings}" + const val sodium_bindings = + "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android:${Versions.sodium_bindings}" const val fingerprint_pro = "com.fingerprint.android:pro:2.4.0" + + const val haze = "dev.chrisbanes.haze:haze:0.7.3" + const val process_phoenix = "com.jakewharton:process-phoenix:3.0.0" } diff --git a/common/resources/build.gradle.kts b/common/resources/build.gradle.kts index 200009ec8..e8cac4b91 100644 --- a/common/resources/build.gradle.kts +++ b/common/resources/build.gradle.kts @@ -14,6 +14,14 @@ android { testInstrumentationRunner = Android.testInstrumentationRunner } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose_compiler + } + kotlinOptions { jvmTarget = Versions.java freeCompilerArgs += listOf( @@ -32,13 +40,12 @@ android { } dependencies { - api(project(":model")) - api(project(":ed25519")) - api(Libs.androidx_annotation) api(Libs.kotlin_stdlib) api(Libs.kotlinx_coroutines_core) api(Libs.kotlinx_coroutines_rx3) - api(Libs.kin_sdk) + implementation(platform(Libs.compose_bom)) + implementation(Libs.compose_ui) + implementation(Libs.compose_foundation) } diff --git a/common/resources/src/main/java/com/getcode/util/resources/icons/ImageVector.kt b/common/resources/src/main/java/com/getcode/util/resources/icons/ImageVector.kt new file mode 100644 index 000000000..c7aaf8f29 --- /dev/null +++ b/common/resources/src/main/java/com/getcode/util/resources/icons/ImageVector.kt @@ -0,0 +1,77 @@ +package com.getcode.util.resources.icons + +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.VectorGroup +import androidx.compose.ui.graphics.vector.VectorNode +import androidx.compose.ui.graphics.vector.VectorPath +import androidx.compose.ui.graphics.vector.group + +fun ImageVector.Companion.copyFrom( + src: ImageVector, + mirror: Boolean = false, + rotation: Float = src.root.rotation, + pivotX: Float = src.defaultWidth.value / 2, + pivotY: Float = src.defaultHeight.value / 2, +) = ImageVector.Builder( + name = src.name, + defaultWidth = src.defaultWidth, + defaultHeight = src.defaultHeight, + viewportWidth = src.viewportWidth, + viewportHeight = src.viewportHeight, + tintColor = src.tintColor, + tintBlendMode = src.tintBlendMode, + autoMirror = src.autoMirror, +).addGroup( + src = src.root, + rotation = rotation, + pivotX = pivotX, + pivotY = pivotY, + scaleX = if (mirror) -1f else 1f, + scaleY = if (mirror) 1f else 1f, +).build() + +private fun ImageVector.Builder.addNode(node: VectorNode) { + when (node) { + is VectorGroup -> addGroup(node) + is VectorPath -> addPath(node) + } +} + +private fun ImageVector.Builder.addGroup( + src: VectorGroup, + rotation: Float = src.rotation, + pivotX: Float = src.pivotX, + pivotY: Float = src.pivotY, + scaleX: Float = src.scaleX, + scaleY: Float = src.scaleY, +) = apply { + group( + name = src.name, + rotate = rotation, + pivotX = pivotX, + pivotY = pivotY, + scaleX = scaleX, + scaleY = scaleY, + translationX = src.translationX, + translationY = src.translationY, + clipPathData = src.clipPathData, + ) { + src.forEach { addNode(it) } + } +} + +private fun ImageVector.Builder.addPath(src: VectorPath) = apply { + addPath( + pathData = src.pathData, + pathFillType = src.pathFillType, + name = src.name, + fill = src.fill, + fillAlpha = src.fillAlpha, + stroke = src.stroke, + strokeAlpha = src.strokeAlpha, + strokeLineWidth = src.strokeLineWidth, + strokeLineCap = src.strokeLineCap, + strokeLineJoin = src.strokeLineJoin, + strokeLineMiter = src.strokeLineMiter, + ) +} \ No newline at end of file diff --git a/common/resources/src/main/java/com/getcode/util/resources/icons/MessageCircle.kt b/common/resources/src/main/java/com/getcode/util/resources/icons/MessageCircle.kt new file mode 100644 index 000000000..961853221 --- /dev/null +++ b/common/resources/src/main/java/com/getcode/util/resources/icons/MessageCircle.kt @@ -0,0 +1,52 @@ +package com.getcode.util.resources.icons + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +private var _MessageCircle: ImageVector? = null +val MessageCircle: ImageVector + get() { + if (_MessageCircle != null) { + return _MessageCircle!! + } + _MessageCircle = ImageVector.Builder( + name = "MessageCircle", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = null, + fillAlpha = 1.0f, + stroke = SolidColor(Color(0xFFFFFFFF)), + strokeAlpha = 1.0f, + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round, + strokeLineMiter = 1.0f, + pathFillType = PathFillType.NonZero + ) { + moveTo(7.9f, 20f) + arcTo(9f, 9f, 0f, isMoreThanHalf = true, isPositiveArc = false, 4f, 16.1f) + lineTo(2f, 22f) + close() + } + }.build() + return _MessageCircle!! + } + +val AutoMirroredMessageCircle: ImageVector + @Composable get() = ImageVector.copyFrom(MessageCircle, mirror = LocalLayoutDirection.current == LayoutDirection.Ltr) + +val MirroredMessageCircle: ImageVector + get() = ImageVector.copyFrom(MessageCircle, mirror = true) \ No newline at end of file diff --git a/common/theme/src/main/kotlin/com/getcode/theme/Color.kt b/common/theme/src/main/kotlin/com/getcode/theme/Color.kt index 1879d653b..951c78b8e 100644 --- a/common/theme/src/main/kotlin/com/getcode/theme/Color.kt +++ b/common/theme/src/main/kotlin/com/getcode/theme/Color.kt @@ -8,7 +8,7 @@ val BrandLight = Color(0xFF7379A0) val BrandSubtle = Color(0xFF565C86) val BrandMuted = Color(0xFF45464E) val BrandDark = Color(0xFF1F1A34) -val BrandOverlay = Color(0xBF1E1E1E) +val BrandAction = Color(0xFF212121) val Brand01 = Color(0xFF130F27) val White = Color(0xffffffff) diff --git a/common/theme/src/main/kotlin/com/getcode/theme/Theme.kt b/common/theme/src/main/kotlin/com/getcode/theme/Theme.kt index 71aa9583e..cabc2784f 100644 --- a/common/theme/src/main/kotlin/com/getcode/theme/Theme.kt +++ b/common/theme/src/main/kotlin/com/getcode/theme/Theme.kt @@ -23,6 +23,8 @@ private val DarkColorPalette = CodeColors( brandLight = BrandLight, brandSubtle = BrandSubtle, brandMuted = BrandMuted, + action = Gray50, + onAction = White, background = Brand, onBackground = White, surface = Brand, @@ -82,6 +84,8 @@ class CodeColors( brandLight: Color, brandSubtle: Color, brandMuted: Color, + action: Color, + onAction: Color, background: Color, onBackground: Color, surface: Color, @@ -99,6 +103,10 @@ class CodeColors( private set var brandMuted by mutableStateOf(brandMuted) private set + var action by mutableStateOf(action) + private set + var onAction by mutableStateOf(onAction) + private set var background by mutableStateOf(background) private set var onBackground by mutableStateOf(onBackground) @@ -121,6 +129,7 @@ class CodeColors( brandLight = other.brandLight brandSubtle = other.brandSubtle brandMuted = other.brandMuted + action = other.action background = other.background onBackground = other.onBackground surface = other.surface @@ -136,6 +145,8 @@ class CodeColors( brandLight = brandLight, brandSubtle = brandSubtle, brandMuted = brandMuted, + action = action, + onAction = onAction, background = background, onBackground = onBackground, surface = surface, diff --git a/ed25519/.gitignore b/crypto/ed25519/.gitignore similarity index 100% rename from ed25519/.gitignore rename to crypto/ed25519/.gitignore diff --git a/ed25519/CMakeLists.txt b/crypto/ed25519/CMakeLists.txt similarity index 100% rename from ed25519/CMakeLists.txt rename to crypto/ed25519/CMakeLists.txt diff --git a/ed25519/build.gradle.kts b/crypto/ed25519/build.gradle.kts similarity index 100% rename from ed25519/build.gradle.kts rename to crypto/ed25519/build.gradle.kts diff --git a/ed25519/libs/base64/CMakeLists.txt b/crypto/ed25519/libs/base64/CMakeLists.txt similarity index 100% rename from ed25519/libs/base64/CMakeLists.txt rename to crypto/ed25519/libs/base64/CMakeLists.txt diff --git a/ed25519/libs/base64/base64.cpp b/crypto/ed25519/libs/base64/base64.cpp similarity index 100% rename from ed25519/libs/base64/base64.cpp rename to crypto/ed25519/libs/base64/base64.cpp diff --git a/ed25519/libs/base64/base64.hpp b/crypto/ed25519/libs/base64/base64.hpp similarity index 100% rename from ed25519/libs/base64/base64.hpp rename to crypto/ed25519/libs/base64/base64.hpp diff --git a/ed25519/libs/codeScanner/CMakeLists.txt b/crypto/ed25519/libs/codeScanner/CMakeLists.txt similarity index 100% rename from ed25519/libs/codeScanner/CMakeLists.txt rename to crypto/ed25519/libs/codeScanner/CMakeLists.txt diff --git a/ed25519/libs/codeScanner/src/Array.h b/crypto/ed25519/libs/codeScanner/src/Array.h similarity index 100% rename from ed25519/libs/codeScanner/src/Array.h rename to crypto/ed25519/libs/codeScanner/src/Array.h diff --git a/ed25519/libs/codeScanner/src/Counted.h b/crypto/ed25519/libs/codeScanner/src/Counted.h similarity index 100% rename from ed25519/libs/codeScanner/src/Counted.h rename to crypto/ed25519/libs/codeScanner/src/Counted.h diff --git a/ed25519/libs/codeScanner/src/Exception.cpp b/crypto/ed25519/libs/codeScanner/src/Exception.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/Exception.cpp rename to crypto/ed25519/libs/codeScanner/src/Exception.cpp diff --git a/ed25519/libs/codeScanner/src/Exception.h b/crypto/ed25519/libs/codeScanner/src/Exception.h similarity index 100% rename from ed25519/libs/codeScanner/src/Exception.h rename to crypto/ed25519/libs/codeScanner/src/Exception.h diff --git a/ed25519/libs/codeScanner/src/GenericGF.cpp b/crypto/ed25519/libs/codeScanner/src/GenericGF.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/GenericGF.cpp rename to crypto/ed25519/libs/codeScanner/src/GenericGF.cpp diff --git a/ed25519/libs/codeScanner/src/GenericGF.h b/crypto/ed25519/libs/codeScanner/src/GenericGF.h similarity index 100% rename from ed25519/libs/codeScanner/src/GenericGF.h rename to crypto/ed25519/libs/codeScanner/src/GenericGF.h diff --git a/ed25519/libs/codeScanner/src/GenericGFPoly.cpp b/crypto/ed25519/libs/codeScanner/src/GenericGFPoly.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/GenericGFPoly.cpp rename to crypto/ed25519/libs/codeScanner/src/GenericGFPoly.cpp diff --git a/ed25519/libs/codeScanner/src/GenericGFPoly.h b/crypto/ed25519/libs/codeScanner/src/GenericGFPoly.h similarity index 100% rename from ed25519/libs/codeScanner/src/GenericGFPoly.h rename to crypto/ed25519/libs/codeScanner/src/GenericGFPoly.h diff --git a/ed25519/libs/codeScanner/src/IllegalArgumentException.cpp b/crypto/ed25519/libs/codeScanner/src/IllegalArgumentException.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/IllegalArgumentException.cpp rename to crypto/ed25519/libs/codeScanner/src/IllegalArgumentException.cpp diff --git a/ed25519/libs/codeScanner/src/IllegalArgumentException.h b/crypto/ed25519/libs/codeScanner/src/IllegalArgumentException.h similarity index 100% rename from ed25519/libs/codeScanner/src/IllegalArgumentException.h rename to crypto/ed25519/libs/codeScanner/src/IllegalArgumentException.h diff --git a/ed25519/libs/codeScanner/src/IllegalStateException.h b/crypto/ed25519/libs/codeScanner/src/IllegalStateException.h similarity index 100% rename from ed25519/libs/codeScanner/src/IllegalStateException.h rename to crypto/ed25519/libs/codeScanner/src/IllegalStateException.h diff --git a/ed25519/libs/codeScanner/src/ReaderException.h b/crypto/ed25519/libs/codeScanner/src/ReaderException.h similarity index 100% rename from ed25519/libs/codeScanner/src/ReaderException.h rename to crypto/ed25519/libs/codeScanner/src/ReaderException.h diff --git a/ed25519/libs/codeScanner/src/ReedSolomonDecoder.cpp b/crypto/ed25519/libs/codeScanner/src/ReedSolomonDecoder.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonDecoder.cpp rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonDecoder.cpp diff --git a/ed25519/libs/codeScanner/src/ReedSolomonDecoder.h b/crypto/ed25519/libs/codeScanner/src/ReedSolomonDecoder.h similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonDecoder.h rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonDecoder.h diff --git a/ed25519/libs/codeScanner/src/ReedSolomonEncoder.cpp b/crypto/ed25519/libs/codeScanner/src/ReedSolomonEncoder.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonEncoder.cpp rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonEncoder.cpp diff --git a/ed25519/libs/codeScanner/src/ReedSolomonEncoder.h b/crypto/ed25519/libs/codeScanner/src/ReedSolomonEncoder.h similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonEncoder.h rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonEncoder.h diff --git a/ed25519/libs/codeScanner/src/ReedSolomonException.cpp b/crypto/ed25519/libs/codeScanner/src/ReedSolomonException.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonException.cpp rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonException.cpp diff --git a/ed25519/libs/codeScanner/src/ReedSolomonException.h b/crypto/ed25519/libs/codeScanner/src/ReedSolomonException.h similarity index 100% rename from ed25519/libs/codeScanner/src/ReedSolomonException.h rename to crypto/ed25519/libs/codeScanner/src/ReedSolomonException.h diff --git a/ed25519/libs/codeScanner/src/ZXing.h b/crypto/ed25519/libs/codeScanner/src/ZXing.h similarity index 100% rename from ed25519/libs/codeScanner/src/ZXing.h rename to crypto/ed25519/libs/codeScanner/src/ZXing.h diff --git a/ed25519/libs/codeScanner/src/kikcode_constants.h b/crypto/ed25519/libs/codeScanner/src/kikcode_constants.h similarity index 100% rename from ed25519/libs/codeScanner/src/kikcode_constants.h rename to crypto/ed25519/libs/codeScanner/src/kikcode_constants.h diff --git a/ed25519/libs/codeScanner/src/kikcode_encoding.cpp b/crypto/ed25519/libs/codeScanner/src/kikcode_encoding.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/kikcode_encoding.cpp rename to crypto/ed25519/libs/codeScanner/src/kikcode_encoding.cpp diff --git a/ed25519/libs/codeScanner/src/kikcode_encoding.h b/crypto/ed25519/libs/codeScanner/src/kikcode_encoding.h similarity index 100% rename from ed25519/libs/codeScanner/src/kikcode_encoding.h rename to crypto/ed25519/libs/codeScanner/src/kikcode_encoding.h diff --git a/ed25519/libs/codeScanner/src/kikcode_encoding_jni.cpp b/crypto/ed25519/libs/codeScanner/src/kikcode_encoding_jni.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/kikcode_encoding_jni.cpp rename to crypto/ed25519/libs/codeScanner/src/kikcode_encoding_jni.cpp diff --git a/ed25519/libs/codeScanner/src/kikcode_encoding_jni.h b/crypto/ed25519/libs/codeScanner/src/kikcode_encoding_jni.h similarity index 100% rename from ed25519/libs/codeScanner/src/kikcode_encoding_jni.h rename to crypto/ed25519/libs/codeScanner/src/kikcode_encoding_jni.h diff --git a/ed25519/libs/codeScanner/src/kikcodes.cpp b/crypto/ed25519/libs/codeScanner/src/kikcodes.cpp similarity index 100% rename from ed25519/libs/codeScanner/src/kikcodes.cpp rename to crypto/ed25519/libs/codeScanner/src/kikcodes.cpp diff --git a/ed25519/libs/codeScanner/src/kikcodes.h b/crypto/ed25519/libs/codeScanner/src/kikcodes.h similarity index 100% rename from ed25519/libs/codeScanner/src/kikcodes.h rename to crypto/ed25519/libs/codeScanner/src/kikcodes.h diff --git a/ed25519/libs/ed25519/CMakeLists.txt b/crypto/ed25519/libs/ed25519/CMakeLists.txt similarity index 100% rename from ed25519/libs/ed25519/CMakeLists.txt rename to crypto/ed25519/libs/ed25519/CMakeLists.txt diff --git a/ed25519/libs/ed25519/license.txt b/crypto/ed25519/libs/ed25519/license.txt similarity index 100% rename from ed25519/libs/ed25519/license.txt rename to crypto/ed25519/libs/ed25519/license.txt diff --git a/ed25519/libs/ed25519/readme.md b/crypto/ed25519/libs/ed25519/readme.md similarity index 100% rename from ed25519/libs/ed25519/readme.md rename to crypto/ed25519/libs/ed25519/readme.md diff --git a/ed25519/libs/ed25519/src/add_scalar.c b/crypto/ed25519/libs/ed25519/src/add_scalar.c similarity index 100% rename from ed25519/libs/ed25519/src/add_scalar.c rename to crypto/ed25519/libs/ed25519/src/add_scalar.c diff --git a/ed25519/libs/ed25519/src/ed25519.h b/crypto/ed25519/libs/ed25519/src/ed25519.h similarity index 100% rename from ed25519/libs/ed25519/src/ed25519.h rename to crypto/ed25519/libs/ed25519/src/ed25519.h diff --git a/ed25519/libs/ed25519/src/fe.c b/crypto/ed25519/libs/ed25519/src/fe.c similarity index 100% rename from ed25519/libs/ed25519/src/fe.c rename to crypto/ed25519/libs/ed25519/src/fe.c diff --git a/ed25519/libs/ed25519/src/fe.h b/crypto/ed25519/libs/ed25519/src/fe.h similarity index 100% rename from ed25519/libs/ed25519/src/fe.h rename to crypto/ed25519/libs/ed25519/src/fe.h diff --git a/ed25519/libs/ed25519/src/fixedint.h b/crypto/ed25519/libs/ed25519/src/fixedint.h similarity index 100% rename from ed25519/libs/ed25519/src/fixedint.h rename to crypto/ed25519/libs/ed25519/src/fixedint.h diff --git a/ed25519/libs/ed25519/src/ge.c b/crypto/ed25519/libs/ed25519/src/ge.c similarity index 100% rename from ed25519/libs/ed25519/src/ge.c rename to crypto/ed25519/libs/ed25519/src/ge.c diff --git a/ed25519/libs/ed25519/src/ge.h b/crypto/ed25519/libs/ed25519/src/ge.h similarity index 100% rename from ed25519/libs/ed25519/src/ge.h rename to crypto/ed25519/libs/ed25519/src/ge.h diff --git a/ed25519/libs/ed25519/src/key_exchange.c b/crypto/ed25519/libs/ed25519/src/key_exchange.c similarity index 100% rename from ed25519/libs/ed25519/src/key_exchange.c rename to crypto/ed25519/libs/ed25519/src/key_exchange.c diff --git a/ed25519/libs/ed25519/src/keypair.c b/crypto/ed25519/libs/ed25519/src/keypair.c similarity index 100% rename from ed25519/libs/ed25519/src/keypair.c rename to crypto/ed25519/libs/ed25519/src/keypair.c diff --git a/ed25519/libs/ed25519/src/precomp_data.h b/crypto/ed25519/libs/ed25519/src/precomp_data.h similarity index 100% rename from ed25519/libs/ed25519/src/precomp_data.h rename to crypto/ed25519/libs/ed25519/src/precomp_data.h diff --git a/ed25519/libs/ed25519/src/sc.c b/crypto/ed25519/libs/ed25519/src/sc.c similarity index 100% rename from ed25519/libs/ed25519/src/sc.c rename to crypto/ed25519/libs/ed25519/src/sc.c diff --git a/ed25519/libs/ed25519/src/sc.h b/crypto/ed25519/libs/ed25519/src/sc.h similarity index 100% rename from ed25519/libs/ed25519/src/sc.h rename to crypto/ed25519/libs/ed25519/src/sc.h diff --git a/ed25519/libs/ed25519/src/seed.c b/crypto/ed25519/libs/ed25519/src/seed.c similarity index 100% rename from ed25519/libs/ed25519/src/seed.c rename to crypto/ed25519/libs/ed25519/src/seed.c diff --git a/ed25519/libs/ed25519/src/sha3.c b/crypto/ed25519/libs/ed25519/src/sha3.c similarity index 100% rename from ed25519/libs/ed25519/src/sha3.c rename to crypto/ed25519/libs/ed25519/src/sha3.c diff --git a/ed25519/libs/ed25519/src/sha3.h b/crypto/ed25519/libs/ed25519/src/sha3.h similarity index 100% rename from ed25519/libs/ed25519/src/sha3.h rename to crypto/ed25519/libs/ed25519/src/sha3.h diff --git a/ed25519/libs/ed25519/src/sha512.c b/crypto/ed25519/libs/ed25519/src/sha512.c similarity index 100% rename from ed25519/libs/ed25519/src/sha512.c rename to crypto/ed25519/libs/ed25519/src/sha512.c diff --git a/ed25519/libs/ed25519/src/sha512.h b/crypto/ed25519/libs/ed25519/src/sha512.h similarity index 100% rename from ed25519/libs/ed25519/src/sha512.h rename to crypto/ed25519/libs/ed25519/src/sha512.h diff --git a/ed25519/libs/ed25519/src/sign.c b/crypto/ed25519/libs/ed25519/src/sign.c similarity index 100% rename from ed25519/libs/ed25519/src/sign.c rename to crypto/ed25519/libs/ed25519/src/sign.c diff --git a/ed25519/libs/ed25519/src/verify.c b/crypto/ed25519/libs/ed25519/src/verify.c similarity index 100% rename from ed25519/libs/ed25519/src/verify.c rename to crypto/ed25519/libs/ed25519/src/verify.c diff --git a/ed25519/libs/ed25519/test.c b/crypto/ed25519/libs/ed25519/test.c similarity index 100% rename from ed25519/libs/ed25519/test.c rename to crypto/ed25519/libs/ed25519/test.c diff --git a/ed25519/proguard-rules.pro b/crypto/ed25519/proguard-rules.pro similarity index 100% rename from ed25519/proguard-rules.pro rename to crypto/ed25519/proguard-rules.pro diff --git a/ed25519/src/main/AndroidManifest.xml b/crypto/ed25519/src/main/AndroidManifest.xml similarity index 100% rename from ed25519/src/main/AndroidManifest.xml rename to crypto/ed25519/src/main/AndroidManifest.xml diff --git a/ed25519/src/main/cpp/kik-codes.cpp b/crypto/ed25519/src/main/cpp/kik-codes.cpp similarity index 100% rename from ed25519/src/main/cpp/kik-codes.cpp rename to crypto/ed25519/src/main/cpp/kik-codes.cpp diff --git a/ed25519/src/main/cpp/native-lib.cpp b/crypto/ed25519/src/main/cpp/native-lib.cpp similarity index 100% rename from ed25519/src/main/cpp/native-lib.cpp rename to crypto/ed25519/src/main/cpp/native-lib.cpp diff --git a/ed25519/src/main/java/com/getcode/codeScanner/CodeScanner.java b/crypto/ed25519/src/main/java/com/getcode/codeScanner/CodeScanner.java similarity index 100% rename from ed25519/src/main/java/com/getcode/codeScanner/CodeScanner.java rename to crypto/ed25519/src/main/java/com/getcode/codeScanner/CodeScanner.java diff --git a/ed25519/src/main/java/com/getcode/ed25519/Ed25519.java b/crypto/ed25519/src/main/java/com/getcode/ed25519/Ed25519.java similarity index 100% rename from ed25519/src/main/java/com/getcode/ed25519/Ed25519.java rename to crypto/ed25519/src/main/java/com/getcode/ed25519/Ed25519.java diff --git a/model/.gitignore b/crypto/kin/.gitignore similarity index 100% rename from model/.gitignore rename to crypto/kin/.gitignore diff --git a/crypto/kin/build.gradle.kts b/crypto/kin/build.gradle.kts new file mode 100644 index 000000000..72ac956c8 --- /dev/null +++ b/crypto/kin/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id(Plugins.android_library) + id(Plugins.kotlin_android) + id(Plugins.kotlin_serialization) +} + +android { + namespace = "${Android.namespace}.vendor.kin" + compileSdk = Android.compileSdkVersion + defaultConfig { + minSdk = Android.minSdkVersion + targetSdk = Android.targetSdkVersion + buildToolsVersion = Android.buildToolsVersion + testInstrumentationRunner = Android.testInstrumentationRunner + } + + kotlinOptions { + jvmTarget = Versions.java + freeCompilerArgs += listOf( + "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xuse-experimental=kotlinx.coroutines.FlowPreview", + "-opt-in=kotlin.ExperimentalUnsignedTypes", + "-opt-in=kotlin.RequiresOptIn" + ) + } + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(Versions.java)) + } + } +} + +dependencies { + api(Libs.kin_sdk) +} diff --git a/model/build.gradle b/model/build.gradle deleted file mode 100644 index a8a13e934..000000000 --- a/model/build.gradle +++ /dev/null @@ -1,104 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os - -plugins { - id 'java-library' - id 'com.google.protobuf' - id 'maven-publish' -} - -def archSuffix = Os.isFamily(Os.FAMILY_MAC) ? ':osx-x86_64' : '' - -version = '0.0.1' -group = 'com.codeinc.gen' - -def grpcVersion = "1.49.0" -def protocVersion = "3.12.0" -dependencies { - implementation "io.grpc:grpc-protobuf-lite:$grpcVersion" // "io.grpc:grpc-protobuf-lite:${grpcVersion}" - implementation "io.grpc:grpc-stub:$grpcVersion" -// protobuf("io.envoyproxy.protoc-gen-validate:pgv-java-stub:${validateVersion}") { -// exclude group: "com.google.protobuf", module: "protobuf-java" -// exclude group: "com.google.protobuf", module: "protobuf-java-util" -// } - - // Kotlin Generation - implementation "io.grpc:grpc-kotlin-stub-lite:1.0.0" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3" - implementation 'javax.annotation:javax.annotation-api:1.3.2' - -} - -def generatedSrcRoot = file("${buildDir}/generated/source/proto/main/java") -def generatedSrcRoot2 = file("${buildDir}/generated/source/proto/main/grpc") -def generatedSrcRoot3 = file("${buildDir}/generated/source/proto/main/grpcKt") -sourceSets.main { - proto { - srcDir "${rootDir}/model/proto" - } - java { - srcDirs = ["$generatedSrcRoot", "$generatedSrcRoot2", "$generatedSrcRoot3"] - } -} - -compileJava { - options.annotationProcessorGeneratedSourcesDirectory generatedSrcRoot -} - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${protocVersion}$archSuffix" - } - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" - } -// javapgv { -// artifact = "io.envoyproxy.protoc-gen-validate:protoc-gen-validate:${validateVersion}" -// } - grpcKt { - artifact = "io.grpc:protoc-gen-grpc-kotlin:1.0.0:jdk7@jar" - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - java { - option "lite" - } - } - } - all()*.plugins { - grpc { - option 'lite' - } - grpcKt { - option 'lite' - } - } - } -} - -jar { - manifest { - attributes('code-api': project.name, - 'Version': project.version) - } - - exclude "**/*agora*/**" -} - -task sourcesJar(type: Jar) { - from sourceSets.main.allSource - archiveClassifier = 'sources' - from generatedSrcRoot, generatedSrcRoot2, generatedSrcRoot3 - exclude "**/*agora*/**" -} - -artifacts { - archives sourcesJar -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} \ No newline at end of file diff --git a/model/remove_proto_validation.sh b/model/remove_proto_validation.sh deleted file mode 100755 index 783c9bfc5..000000000 --- a/model/remove_proto_validation.sh +++ /dev/null @@ -1,21 +0,0 @@ -# -# For all .proto files, strip the validation parameters & replace inline -# - -# 1. hack: first add a couple newlines after all "];" -find proto/ -name "*.proto" -type f -exec sh -c "awk '{gsub(/];/, \"];\n\n\"); print}' {} > tmp && mv tmp {}" \; - -# 2. strip everything between square brackets [...] -find proto/ -name "*.proto" -type f -exec sh -c "awk -v RS='' '{gsub(/ \[.*\]/, \"\"); print}' {} > tmp && mv tmp {}" \; - -# 3. add a newline after all trailing } brackets -find proto/ -name "*.proto" -type f -exec sh -c "awk '{gsub(/}/, \"}\n\"); print}' {} > tmp && mv tmp {}" \; - -# 4. strip validate import statement -find proto/ -name "*.proto" -type f -exec sh -c "awk -v RS='' '{gsub(/import \"validate\/validate.proto\";/, \"\"); print}' {} > tmp && mv tmp {}" \; - -# 5. strip validate required options -find proto/ -name "*.proto" -type f -exec sh -c "awk -v RS='' '{gsub(/option \(validate.required\) = true;/, \"\"); print}' {} > tmp && mv tmp {}" \; - -# 6. add a newline after all trailing } brackets -find proto/ -name "*.proto" -type f -exec sh -c "awk '{gsub(/}/, \"}\n\"); print}' {} > tmp && mv tmp {}" \; diff --git a/service/models/.gitignore b/service/models/.gitignore new file mode 100644 index 000000000..9f2a07880 --- /dev/null +++ b/service/models/.gitignore @@ -0,0 +1,2 @@ +build/ +.gradle/ diff --git a/service/models/build.gradle.kts b/service/models/build.gradle.kts new file mode 100644 index 000000000..90238dade --- /dev/null +++ b/service/models/build.gradle.kts @@ -0,0 +1,77 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +plugins { + id(Plugins.android_library) + id(Plugins.kotlin_android) + id("com.google.protobuf") +} + +val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" + +version = "0.0.1" +group = "com.codeinc.gen" + +dependencies { + protobuf(project(":service:protos")) + + implementation(Libs.grpc_protobuf_lite) + implementation(Libs.grpc_stub) + + // Kotlin Generation + implementation(Libs.grpc_kotlin) + implementation(Libs.protobuf_kotlin_lite) + implementation(Libs.kotlinx_coroutines_core) +} + +kotlin { + jvmToolchain(17) +} + +android { + compileSdk = 34 + buildToolsVersion = "34.0.0" + namespace = "io.grpc.examples.stublite" +} + +tasks.withType().all { + kotlinOptions { + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${Versions.protobuf}$archSuffix" + } + plugins { + create("java") { + artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" + } + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" + } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("java") { + option("lite") + } + create("grpc") { + option("lite") + } + create("grpckt") { + option("lite") + } + } + it.builtins { + create("kotlin") { + option("lite") + } + } + } + } +} \ No newline at end of file diff --git a/service/protos/.gitignore b/service/protos/.gitignore new file mode 100644 index 000000000..9f2a07880 --- /dev/null +++ b/service/protos/.gitignore @@ -0,0 +1,2 @@ +build/ +.gradle/ diff --git a/service/protos/build.gradle.kts b/service/protos/build.gradle.kts new file mode 100644 index 000000000..4cd4475f1 --- /dev/null +++ b/service/protos/build.gradle.kts @@ -0,0 +1,11 @@ +// todo: maybe use variants / configurations to do both stub & stub-lite here + +// Note: We use the java-library plugin to get the protos into the artifact for this subproject +// because there doesn't seem to be an better way. +plugins { + `java-library` +} + +java { + sourceSets.getByName("main").resources.srcDir("src/main/proto") +} \ No newline at end of file diff --git a/model/proto/account/v1/account_service.proto b/service/protos/src/main/proto/account/v1/account_service.proto similarity index 100% rename from model/proto/account/v1/account_service.proto rename to service/protos/src/main/proto/account/v1/account_service.proto diff --git a/model/proto/badge/v1/badge_service.proto b/service/protos/src/main/proto/badge/v1/badge_service.proto similarity index 100% rename from model/proto/badge/v1/badge_service.proto rename to service/protos/src/main/proto/badge/v1/badge_service.proto diff --git a/model/proto/chat/v1/chat_service.proto b/service/protos/src/main/proto/chat/v1/chat_service.proto similarity index 100% rename from model/proto/chat/v1/chat_service.proto rename to service/protos/src/main/proto/chat/v1/chat_service.proto diff --git a/model/proto/chat/v2/chat_service.proto b/service/protos/src/main/proto/chat/v2/chat_service.proto similarity index 100% rename from model/proto/chat/v2/chat_service.proto rename to service/protos/src/main/proto/chat/v2/chat_service.proto diff --git a/model/proto/common/v1/model.proto b/service/protos/src/main/proto/common/v1/model.proto similarity index 100% rename from model/proto/common/v1/model.proto rename to service/protos/src/main/proto/common/v1/model.proto diff --git a/model/proto/contact/v1/contact_list_service.proto b/service/protos/src/main/proto/contact/v1/contact_list_service.proto similarity index 100% rename from model/proto/contact/v1/contact_list_service.proto rename to service/protos/src/main/proto/contact/v1/contact_list_service.proto diff --git a/model/proto/currency/v1/currency_service.proto b/service/protos/src/main/proto/currency/v1/currency_service.proto similarity index 100% rename from model/proto/currency/v1/currency_service.proto rename to service/protos/src/main/proto/currency/v1/currency_service.proto diff --git a/model/proto/device/v1/device_service.proto b/service/protos/src/main/proto/device/v1/device_service.proto similarity index 100% rename from model/proto/device/v1/device_service.proto rename to service/protos/src/main/proto/device/v1/device_service.proto diff --git a/model/proto/invite/v2/invite_service.proto b/service/protos/src/main/proto/invite/v2/invite_service.proto similarity index 100% rename from model/proto/invite/v2/invite_service.proto rename to service/protos/src/main/proto/invite/v2/invite_service.proto diff --git a/model/proto/messaging/v1/messaging_service.proto b/service/protos/src/main/proto/messaging/v1/messaging_service.proto similarity index 100% rename from model/proto/messaging/v1/messaging_service.proto rename to service/protos/src/main/proto/messaging/v1/messaging_service.proto diff --git a/model/proto/micropayment/v1/micro_payment_service.proto b/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto similarity index 100% rename from model/proto/micropayment/v1/micro_payment_service.proto rename to service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto diff --git a/model/proto/phone/v1/phone_verification_service.proto b/service/protos/src/main/proto/phone/v1/phone_verification_service.proto similarity index 100% rename from model/proto/phone/v1/phone_verification_service.proto rename to service/protos/src/main/proto/phone/v1/phone_verification_service.proto diff --git a/model/proto/push/v1/push_service.proto b/service/protos/src/main/proto/push/v1/push_service.proto similarity index 100% rename from model/proto/push/v1/push_service.proto rename to service/protos/src/main/proto/push/v1/push_service.proto diff --git a/model/proto/transaction/v2/transaction_service.proto b/service/protos/src/main/proto/transaction/v2/transaction_service.proto similarity index 100% rename from model/proto/transaction/v2/transaction_service.proto rename to service/protos/src/main/proto/transaction/v2/transaction_service.proto diff --git a/model/proto/user/v1/identity_service.proto b/service/protos/src/main/proto/user/v1/identity_service.proto similarity index 100% rename from model/proto/user/v1/identity_service.proto rename to service/protos/src/main/proto/user/v1/identity_service.proto diff --git a/settings.gradle b/settings.gradle index f3b37c9f1..841868ca0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,10 @@ -include ':model' include ':api' include ':app' -include ':ed25519' +include ':crypto:kin' +include ':crypto:ed25519' include ':common:resources' +include ':service:models' +include ':service:protos' include ':common:theme' include ':vendor:tipkit:tipkit' include ':vendor:tipkit:tipkit-m2'