diff --git a/.github/workflows/build-upload-android-alpha.yml b/.github/workflows/build-fc-upload-android-internal.yml
similarity index 75%
rename from .github/workflows/build-upload-android-alpha.yml
rename to .github/workflows/build-fc-upload-android-internal.yml
index 3c239aee6..ca71c0098 100644
--- a/.github/workflows/build-upload-android-alpha.yml
+++ b/.github/workflows/build-fc-upload-android-internal.yml
@@ -1,17 +1,13 @@
-name: Android Build and Deploy (Alpha)
+name: Flipchat Build and Deploy (Internal)
env:
# The name of the main module repository
- main_project_module: app
+ main_project_module: flipchatApp
# The name of the Play Store
- playstore_name: Code Wallet
+ playstore_name: Flipchat
on:
- push:
- tags:
- - 'v*'
-
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -42,8 +38,8 @@ jobs:
id: google_services_json_file
with:
fileName: google-services.json
- fileDir: ./app/src
- encodedString: ${{ secrets.GOOGLE_SERVICES_JSON }}
+ fileDir: ./flipchatApp/src
+ encodedString: ${{ secrets.FLIPCHAT_GOOGLE_SERVICES_JSON }}
- name: Decode Service Account Key JSON file
uses: timheuer/base64-to-file@v1
@@ -63,11 +59,11 @@ jobs:
uses: timheuer/base64-to-file@v1
with:
fileName: key
- fileDir: ./app/key
+ fileDir: ./flipchatApp/key
encodedString: ${{ secrets.UPLOAD_KEY_STORE }}
- name: Setup BugSnag API Key
- run: echo BUGSNAG_API_KEY=\"${{ secrets.BUGSNAG_API_KEY }}\" > ./local.properties
+ run: echo BUGSNAG_API_KEY=\"${{ secrets.FLIPCHAT_BUGSNAG_API_KEY }}\" > ./local.properties
- name: Setup Fingerprint API Key
run: echo FINGERPRINT_API_KEY=${{ secrets.FINGERPRINT_API_KEY }} >> ./local.properties
@@ -79,13 +75,13 @@ jobs:
run: echo KADO_API_KEY=\"${{ secrets.KADO_API_KEY }}\" >> ./local.properties
- name: Setup Mixpanel API Key
- run: echo MIXPANEL_API_KEY=\"${{ secrets.MIXPANEL_API_KEY }}\" >> ./local.properties
+ run: echo MIXPANEL_API_KEY=\"${{ secrets.FLIPCHAT_MIXPANEL_API_KEY }}\" >> ./local.properties
- name: Run tests
- run: bundle exec fastlane android test
+ run: bundle exec fastlane android fc_tests
- name: Build & deploy Android release
- run: bundle exec fastlane android deploy_alpha
+ run: bundle exec fastlane android deploy_fc_internal
env:
STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS}}
@@ -93,8 +89,10 @@ jobs:
SERVICE_ACCOUNT_KEY_JSON: ${{ steps.service_account_json_file.outputs.filePath }}
- name: Upload build artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
+ if: always()
with:
name: assets
path: |
- ${{ github.workspace }}/app/build/outputs/bundle/release
+ ${{ github.workspace }}/flipchatApp/build/outputs/bundle/release
+ ${{ github.workspace }}/flipchatApp/build/outputs/apk/release
diff --git a/.github/workflows/build-upload-android-internal.yml b/.github/workflows/build-upload-android-internal.yml
index 8e5d87e4f..a28897a8c 100644
--- a/.github/workflows/build-upload-android-internal.yml
+++ b/.github/workflows/build-upload-android-internal.yml
@@ -1,4 +1,4 @@
-name: Android Build and Deploy (Internal)
+name: Code Build and Deploy (Internal)
env:
# The name of the main module repository
@@ -78,10 +78,10 @@ jobs:
run: echo MIXPANEL_API_KEY=\"${{ secrets.MIXPANEL_API_KEY }}\" >> ./local.properties
- name: Run tests
- run: bundle exec fastlane android test
+ run: bundle exec fastlane android code_tests
- name: Build & deploy Android release
- run: bundle exec fastlane android deploy_internal
+ run: bundle exec fastlane android deploy_code_internal
env:
STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS}}
@@ -90,6 +90,7 @@ jobs:
- name: Upload build artifacts
uses: actions/upload-artifact@v4
+ if: always()
with:
name: assets
path: |
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e47e798e3..665edf16d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,10 +11,9 @@ env:
JAVA_VERSION: 17
jobs:
- unit-tests:
- name: Unit tests
+ code-tests:
+ name: Run Code Tests
runs-on: ubuntu-latest
- timeout-minutes: 60
steps:
- uses: actions/checkout@master
@@ -38,7 +37,7 @@ jobs:
fileName: google-services.json
fileDir: ./app/src
encodedString: ${{ secrets.GOOGLE_SERVICES_JSON }}
-
+
- name: Setup BugSnag API Key
run: echo BUGSNAG_API_KEY=\"${{ secrets.BUGSNAG_API_KEY }}\" > ./local.properties
@@ -54,5 +53,50 @@ jobs:
- name: Setup Mixpanel API Key
run: echo MIXPANEL_API_KEY=\"${{ secrets.MIXPANEL_API_KEY }}\" >> ./local.properties
- - name: Run tests
- run: bundle exec fastlane android test
+ - name: Run Code tests
+ run: bundle exec fastlane android code_tests
+
+ fc-tests:
+ name: Run Flipchat Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+
+ - name: Setup Java env
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'corretto'
+ cache: 'gradle'
+
+ - name: Setup Ruby env
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 2.7.2
+ bundler-cache: true
+
+ - name: Decode Google Services JSON file
+ uses: timheuer/base64-to-file@v1
+ id: google_services_json_file
+ with:
+ fileName: google-services.json
+ fileDir: ./flipchatApp/src
+ encodedString: ${{ secrets.FLIPCHAT_GOOGLE_SERVICES_JSON }}
+
+ - name: Setup BugSnag API Key
+ run: echo BUGSNAG_API_KEY=\"${{ secrets.FLIPCHAT_BUGSNAG_API_KEY }}\" > ./local.properties
+
+ - name: Setup Fingerprint API Key
+ run: echo FINGERPRINT_API_KEY=${{ secrets.FINGERPRINT_API_KEY }} >> ./local.properties
+
+ - name: Setup Google Cloud Project Number
+ run: echo GOOGLE_CLOUD_PROJECT_NUMBER=${{ secrets.GOOGLE_CLOUD_PROJECT_NUMBER }} >> ./local.properties
+
+ - name: Setup Kado API Key
+ run: echo KADO_API_KEY=\"${{ secrets.KADO_API_KEY }}\" >> ./local.properties
+
+ - name: Setup Mixpanel API Key
+ run: echo MIXPANEL_API_KEY=\"${{ secrets.FLIPCHAT_MIXPANEL_API_KEY }}\" >> ./local.properties
+
+ - name: Run Code tests
+ run: bundle exec fastlane android fc_tests
diff --git a/Gemfile b/Gemfile
index 7a118b49b..cdd3a6b34 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
+
+plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
+eval_gemfile(plugins_path) if File.exist?(plugins_path)
diff --git a/Gemfile.lock b/Gemfile.lock
index ddced0c39..b4e1188f6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -105,6 +105,7 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
+ fastlane-plugin-bundletool (1.0.12)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.37.0)
google-apis-core (>= 0.11.0, < 2.a)
@@ -209,11 +210,13 @@ GEM
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
+ arm64-darwin-24
universal-darwin-21
x86_64-linux
DEPENDENCIES
fastlane
+ fastlane-plugin-bundletool
BUNDLED WITH
2.2.6
diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml
deleted file mode 100644
index 9091cdd71..000000000
--- a/api/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
diff --git a/api/src/main/java/com/getcode/crypt/KeyAccount.kt b/api/src/main/java/com/getcode/crypt/KeyAccount.kt
deleted file mode 100644
index 3582bb008..000000000
--- a/api/src/main/java/com/getcode/crypt/KeyAccount.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.getcode.crypt
-
-import android.content.Context
-import com.getcode.solana.organizer.Organizer
-import com.getcode.solana.keys.PublicKey
-
-class KeyAccount(
- val mnemonic: MnemonicPhrase,
- val derivedKey: DerivedKey,
- val tokenAccount: PublicKey
-) {
- companion object {
- fun newInstance(
- mnemonic: MnemonicPhrase,
- derivedKey: DerivedKey,
- tokenAccount: PublicKey
- ): KeyAccount {
- return KeyAccount(
- mnemonic = mnemonic,
- derivedKey = derivedKey,
- tokenAccount = tokenAccount
- )
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/db/ConversationDao.kt b/api/src/main/java/com/getcode/db/ConversationDao.kt
deleted file mode 100644
index ab0f87182..000000000
--- a/api/src/main/java/com/getcode/db/ConversationDao.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.getcode.db
-
-import androidx.paging.PagingSource
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.RewriteQueriesToDropUnusedColumns
-import com.getcode.model.Conversation
-import com.getcode.model.ConversationWithLastPointers
-import com.getcode.model.ID
-import com.getcode.network.repository.base58
-import kotlinx.coroutines.flow.Flow
-
-@Dao
-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
-
- fun observeConversation(id: ID): Flow {
- return observeConversation(id.base58)
- }
-
- @RewriteQueriesToDropUnusedColumns
- @Query("SELECT * FROM conversations LEFT JOIN conversation_pointers ON conversations.idBase58 = conversation_pointers.conversationIdBase58 WHERE conversations.idBase58 = :id")
- suspend fun findConversation(id: String): ConversationWithLastPointers?
-
- suspend fun findConversation(id: ID): ConversationWithLastPointers? {
- return findConversation(id.base58)
- }
-
- @Query("SELECT * FROM conversations")
- suspend fun queryConversations(): List
-
- @Query("SELECT EXISTS (SELECT 1 FROM messages WHERE conversationIdBase58 = :conversationId)")
- suspend fun hasInteracted(conversationId: String): Boolean
-
- suspend fun hasInteracted(conversationId: ID): Boolean {
- return hasInteracted(conversationId.base58)
- }
-
- @Query("UPDATE conversations SET hasRevealedIdentity = 1 WHERE idBase58 = :conversationId")
- suspend fun revealIdentity(conversationId: String)
-
- suspend fun revealIdentity(conversationId: ID) {
- revealIdentity(conversationId.base58)
- }
-
-// @Query("SELECT EXISTS (SELECT * FROM messages WHERE conversationIdBase58 = :messageId AND content LIKE '%4|%')")
-// suspend fun hasRevealedIdentity(messageId: String): Boolean
-//
-// suspend fun hasRevealedIdentity(messageId: ID): Boolean {
-// return hasRevealedIdentity(messageId.base58)
-// }
-
- @Delete
- fun deleteConversation(conversation: Conversation)
-
- @Query("DELETE FROM conversations WHERE idBase58 = :id")
- suspend fun deleteConversationById(id: String)
-
- suspend fun deleteConversationById(id: ID) {
- deleteConversationById(id.base58)
- }
-
- @Query("DELETE FROM conversations WHERE idBase58 NOT IN (:chatIds)")
- suspend fun purgeConversationsNotInByString(chatIds: List)
- suspend fun purgeConversationsNotIn(chatIds: List) {
- purgeConversationsNotInByString(chatIds.map { it.base58 })
- }
-
- @Query("DELETE FROM conversations")
- fun clearConversations()
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/db/ConversationIntentMappingDao.kt b/api/src/main/java/com/getcode/db/ConversationIntentMappingDao.kt
deleted file mode 100644
index 15e8ae7f0..000000000
--- a/api/src/main/java/com/getcode/db/ConversationIntentMappingDao.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.getcode.db
-
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import com.getcode.model.ConversationIntentIdReference
-import com.getcode.model.ID
-import com.getcode.network.repository.base58
-
-@Dao
-interface ConversationIntentMappingDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insert(mapping: ConversationIntentIdReference)
-
- @Query("SELECT * FROM conversation_intent_id_mapping WHERE intentIdBase58 = :id")
- suspend fun conversationIdByReference(id: String): ConversationIntentIdReference?
-
- suspend fun conversationIdByReference(id: ID): ConversationIntentIdReference? {
- return conversationIdByReference(id.base58)
- }
-
- @Query("DELETE FROM conversation_intent_id_mapping WHERE conversationIdBase58 NOT IN (:chatIds)")
- suspend fun purgeMappingNoLongerNeededByString(chatIds: List)
-
- suspend fun purgeMappingNoLongerNeeded(chatIds: List) {
- purgeMappingNoLongerNeededByString(chatIds.map { it.base58 })
- }
-
- @Query("DELETE FROM conversation_intent_id_mapping")
- suspend fun clearMapping()
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/db/ConversationMessageDao.kt b/api/src/main/java/com/getcode/db/ConversationMessageDao.kt
deleted file mode 100644
index ed2bf2b9e..000000000
--- a/api/src/main/java/com/getcode/db/ConversationMessageDao.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.getcode.db
-
-import androidx.paging.PagingSource
-import androidx.room.Dao
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.RewriteQueriesToDropUnusedColumns
-import androidx.room.Transaction
-import com.getcode.model.ConversationMessage
-import com.getcode.model.ConversationMessageContent
-import com.getcode.model.ConversationMessageWithContent
-import com.getcode.model.ID
-import com.getcode.model.chat.MessageContent
-import com.getcode.network.repository.base58
-
-@Dao
-interface ConversationMessageDao {
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun upsertMessages(vararg message: ConversationMessage)
-
- suspend fun upsertMessages(message: List) {
- upsertMessages(*message.toTypedArray())
- }
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun upsertMessageContent(vararg content: ConversationMessageContent)
-
- suspend fun upsertMessageContent(messageId: ID, contents: List) {
- val contentList = contents.map {
- ConversationMessageContent(messageId.base58, it)
- }
- upsertMessageContent(*contentList.toTypedArray())
- }
-
- @Transaction
- suspend fun upsertMessagesWithContent(vararg message: ConversationMessageWithContent) {
- message.onEach {
- upsertMessages(it.message)
- upsertMessageContent(it.message.id, it.contents)
- }
- }
-
- @Transaction
- suspend fun upsertMessagesWithContent(messages: List) {
- messages.onEach {
- upsertMessages(it.message)
- upsertMessageContent(it.message.id, it.contents)
- }
- }
-
- @RewriteQueriesToDropUnusedColumns
- @Transaction
- @Query("SELECT * FROM messages JOIN message_contents ON messages.idBase58 = message_contents.messageIdBase58 WHERE conversationIdBase58 = :id ORDER BY dateMillis DESC")
- fun observeConversationMessages(id: String): PagingSource
-
- fun observeConversationMessages(id: ID): PagingSource {
- return observeConversationMessages(id.base58)
- }
-
- @Query("SELECT * FROM messages WHERE conversationIdBase58 = :conversationId")
- suspend fun queryMessages(conversationId: String): List
-
- suspend fun queryMessages(conversationId: ID): List {
- return queryMessages(conversationId.base58)
- }
-
- @Query("SELECT * FROM messages WHERE conversationIdBase58 = :conversationId ORDER BY dateMillis DESC LIMIT 1")
- suspend fun getNewestMessage(conversationId: String): ConversationMessage?
-
- suspend fun getNewestMessage(conversationId: ID): ConversationMessage? {
- return getNewestMessage(conversationId.base58)
- }
-
- @Query("DELETE FROM messages WHERE conversationIdBase58 = :conversationId")
- suspend fun deleteForConversation(conversationId: String)
-
- suspend fun deleteForConversation(conversationId: ID) {
- deleteForConversation(conversationId.base58)
- }
-
- @Query("DELETE FROM messages WHERE conversationIdBase58 NOT IN (:chatIds)")
- suspend fun purgeMessagesNotInByString(chatIds: List)
-
- suspend fun purgeMessagesNotIn(chatIds: List) {
- purgeMessagesNotInByString(chatIds.map { it.base58 })
- }
-
- @Query("DELETE FROM messages")
- fun clearMessages()
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/manager/BottomBarManager.kt b/api/src/main/java/com/getcode/manager/BottomBarManager.kt
deleted file mode 100644
index 30f31954d..000000000
--- a/api/src/main/java/com/getcode/manager/BottomBarManager.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.getcode.manager
-
-import kotlinx.coroutines.flow.*
-import java.util.*
-
-
-/**
- * Class responsible for managing BottomBar messages to show on the screen
- */
-object BottomBarManager {
- data class BottomBarMessage(
- val title: String,
- val subtitle: String = "",
- val positiveText: String,
- val negativeText: String,
- val tertiaryText: String? = null,
- val onPositive: () -> Unit,
- val onNegative: () -> Unit = {},
- val onTertiary: () -> Unit = {},
- val onClose: (actionType: BottomBarActionType?) -> Unit = {},
- val type: BottomBarMessageType = BottomBarMessageType.DEFAULT,
- val isDismissible: Boolean = true,
- val timeoutSeconds: Int? = null,
- val id: Long = UUID.randomUUID().mostSignificantBits,
- )
-
- private val _messages: MutableStateFlow> = MutableStateFlow(
- listOf()
- )
- val messages: StateFlow> get() = _messages.asStateFlow()
-
- fun showMessage(bottomBarMessage: BottomBarMessage) {
- _messages.update { currentMessages ->
- currentMessages + bottomBarMessage
- }
- }
-
- fun setMessageShown(messageId: Long) {
- _messages.update { currentMessages ->
- currentMessages.filterNot { it.id == messageId }
- }
- }
-
- fun clear() = _messages.update { listOf() }
-
- fun clearByType(type: BottomBarMessageType) = _messages.update { it.filterNot { m -> m.type == type } }
-
- enum class BottomBarMessageType { DEFAULT, REMOTE_SEND }
-
- enum class BottomBarActionType {
- Positive,
- Negative,
- Tertiary
- }
-
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ChatMemberMapper.kt b/api/src/main/java/com/getcode/mapper/ChatMemberMapper.kt
deleted file mode 100644
index 910f12f3a..000000000
--- a/api/src/main/java/com/getcode/mapper/ChatMemberMapper.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.getcode.mapper
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.model.chat.ChatMember
-import com.getcode.model.chat.Identity
-import com.getcode.model.chat.Pointer
-import com.getcode.model.uuid
-import javax.inject.Inject
-
-class ChatMemberMapper @Inject constructor(): Mapper {
- override fun map(from: ChatService.ChatMember): ChatMember? {
- return ChatMember(
- id = from.memberId.value.toByteArray().toList().uuid ?: return null,
- identity = runCatching { Identity(from.identity) }.getOrNull(),
- isMuted = from.isMuted,
- isSelf = from.isSelf,
- isSubscribed = from.isSubscribed,
- numUnread = from.numUnread,
- pointers = from.pointersList.map { Pointer(it) }
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ChatMessageV1Mapper.kt b/api/src/main/java/com/getcode/mapper/ChatMessageV1Mapper.kt
deleted file mode 100644
index e31dd12a1..000000000
--- a/api/src/main/java/com/getcode/mapper/ChatMessageV1Mapper.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.getcode.mapper
-
-
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.MessageContent
-import javax.inject.Inject
-import com.codeinc.gen.chat.v1.ChatService.ChatMessage as ApiChatMessage
-import com.getcode.model.chat.ChatMessage as DomainChatMessage
-
-@Deprecated("Replace by V2")
-class ChatMessageV1Mapper @Inject constructor(
-): Mapper, DomainChatMessage> {
- override fun map(from: Pair): ChatMessage {
- val (chat, message) = from
-
- val messageId = message.messageId.value.toList()
- val contents = message.contentList.mapNotNull { MessageContent.fromV1(messageId, it) }
- val isFromSelf = contents.firstOrNull { it.isFromSelf } != null
-
- return ChatMessage(
- id = messageId,
- senderId = null,
- isFromSelf = isFromSelf,
- cursor = message.cursor.value.toList(),
- dateMillis = message.ts.seconds * 1_000L,
- contents = contents,
-// status = if (isFromSelf) MessageStatus.Sent else MessageStatus.Incoming
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ChatMessageV2Mapper.kt b/api/src/main/java/com/getcode/mapper/ChatMessageV2Mapper.kt
deleted file mode 100644
index a8f79c459..000000000
--- a/api/src/main/java/com/getcode/mapper/ChatMessageV2Mapper.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.getcode.mapper
-
-
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.MessageContent
-import com.getcode.model.uuid
-import javax.inject.Inject
-import com.codeinc.gen.chat.v2.ChatService.ChatMessage as ApiChatMessage
-import com.getcode.model.chat.ChatMessage as DomainChatMessage
-
-
-class ChatMessageV2Mapper @Inject constructor(
-): Mapper, DomainChatMessage> {
- override fun map(from: Pair): ChatMessage {
- val (chat, message) = from
-
- val messageId = message.messageId.value.toByteArray().toList()
- val messageSenderId = message.senderId.value.toByteArray().toList().uuid
- val selfMember = chat.members.firstOrNull { it.isSelf }
- val isFromSelf = selfMember?.id == messageSenderId
-
- return ChatMessage(
- id = messageId,
- senderId = messageSenderId,
- isFromSelf = isFromSelf,
- cursor = message.cursor.value.toList(),
- dateMillis = message.ts.seconds * 1_000L,
- contents = message.contentList.mapNotNull { MessageContent(it, isFromSelf) },
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ChatMetadataV2Mapper.kt b/api/src/main/java/com/getcode/mapper/ChatMetadataV2Mapper.kt
deleted file mode 100644
index 913aa05af..000000000
--- a/api/src/main/java/com/getcode/mapper/ChatMetadataV2Mapper.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.getcode.mapper
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.ChatType
-import com.getcode.model.chat.Title
-import javax.inject.Inject
-
-class ChatMetadataV2Mapper @Inject constructor(
- private val chatMemberMapper: ChatMemberMapper,
-) : Mapper {
- override fun map(from: ChatService.ChatMetadata): Chat {
- return Chat(
- id = from.chatId.value.toByteArray().toList(),
- title = Title.Localized(from.title),
- cursor = from.cursor.value.toByteArray().toList(),
- canMute = from.canMute,
- canUnsubscribe = from.canUnsubscribe,
- members = from.membersList.mapNotNull { chatMemberMapper.map(it) },
- type = ChatType(from.type),
- messages = emptyList(),
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ConversationMapper.kt b/api/src/main/java/com/getcode/mapper/ConversationMapper.kt
deleted file mode 100644
index 548c58dc9..000000000
--- a/api/src/main/java/com/getcode/mapper/ConversationMapper.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.getcode.mapper
-
-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.localized
-import com.getcode.network.repository.base58
-import com.getcode.util.resources.ResourceHelper
-import javax.inject.Inject
-
-class ConversationMapper @Inject constructor(
- private val resources: ResourceHelper,
-) : Mapper {
- override fun map(from: Chat): Conversation {
-
- val self = from.self?.identity
-
- return Conversation(
- idBase58 = from.id.base58,
- title = when (from.type) {
- ChatType.Unknown,
- ChatType.Notification -> from.title.localized(resources)
- ChatType.TwoWay -> null
- },
- hasRevealedIdentity = self != null,
- members = from.members.map { it },
- lastActivity = null, // TODO: ?
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ConversationMessageMapper.kt b/api/src/main/java/com/getcode/mapper/ConversationMessageMapper.kt
deleted file mode 100644
index 128740ad0..000000000
--- a/api/src/main/java/com/getcode/mapper/ConversationMessageMapper.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.getcode.mapper
-
-import com.getcode.model.ConversationMessage
-import com.getcode.model.ID
-import com.getcode.model.chat.ChatMessage
-import com.getcode.network.repository.base58
-import javax.inject.Inject
-
-class ConversationMessageMapper @Inject constructor() :
- Mapper, ConversationMessage> {
- override fun map(from: Pair): ConversationMessage {
- val (conversationId, message) = from
-
- return ConversationMessage(
- idBase58 = message.id.base58,
- cursorBase58 = message.cursor.base58,
- conversationIdBase58 = conversationId.base58,
- dateMillis = message.dateMillis,
-// status = message.status
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/ConversationMessageWithContentMapper.kt b/api/src/main/java/com/getcode/mapper/ConversationMessageWithContentMapper.kt
deleted file mode 100644
index fb28577fd..000000000
--- a/api/src/main/java/com/getcode/mapper/ConversationMessageWithContentMapper.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.getcode.mapper
-
-import com.getcode.model.ConversationMessageContent
-import com.getcode.model.ConversationMessageWithContent
-import com.getcode.model.ID
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.MessageContent
-import javax.inject.Inject
-
-class ConversationMessageWithContentMapper @Inject constructor(
- private val messageMapper: ConversationMessageMapper,
-): Mapper, ConversationMessageWithContent> {
- override fun map(from: Pair): ConversationMessageWithContent {
- val (_, message) = from
- val conversationMessage = messageMapper.map(from)
-
- return ConversationMessageWithContent(
- message = conversationMessage,
- contents = message.contents
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/mapper/PointerMapper.kt b/api/src/main/java/com/getcode/mapper/PointerMapper.kt
deleted file mode 100644
index b53c30c2c..000000000
--- a/api/src/main/java/com/getcode/mapper/PointerMapper.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.getcode.mapper
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.PointerV2
-import com.getcode.model.uuid
-import com.getcode.utils.timestamp
-import java.util.UUID
-import javax.inject.Inject
-
-data class PointerStatus(
- val messageId: UUID,
- val memberId: UUID,
- val messageStatus: MessageStatus,
-) {
-
- val timestamp: Long?
- get() = messageId.timestamp
-}
-
-class PointerMapper @Inject constructor(): Mapper {
- override fun map(from: PointerV2): PointerStatus? {
- val status = when (from.type) {
- ChatService.PointerType.SENT -> MessageStatus.Sent
- ChatService.PointerType.DELIVERED -> MessageStatus.Delivered
- ChatService.PointerType.READ -> MessageStatus.Read
- else -> MessageStatus.Unknown
- }
-
- val messageId = from.value.value.toByteArray().toList().uuid ?: return null
- val memberId = from.memberId.value.toByteArray().toList().uuid ?: return null
-
- return PointerStatus(
- messageId = messageId,
- memberId = memberId,
- messageStatus = status,
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/Conversation.kt b/api/src/main/java/com/getcode/model/Conversation.kt
deleted file mode 100644
index 29c9107d9..000000000
--- a/api/src/main/java/com/getcode/model/Conversation.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-package com.getcode.model
-
-import androidx.room.ColumnInfo
-import androidx.room.Embedded
-import androidx.room.Entity
-import androidx.room.Ignore
-import androidx.room.PrimaryKey
-import androidx.room.Relation
-import com.getcode.model.chat.ChatMember
-import com.getcode.model.chat.MessageContent
-import com.getcode.utils.serializer.MessageContentSerializer
-import com.getcode.vendor.Base58
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
-import java.util.UUID
-
-@Serializable
-@Entity(tableName = "conversations")
-data class Conversation(
- @PrimaryKey
- val idBase58: String,
- val title: String?,
- val hasRevealedIdentity: Boolean,
- @ColumnInfo(defaultValue = "")
- val members: List,
- val lastActivity: Long?,
-) {
- @Ignore
- val id: ID = Base58.decode(idBase58).toList()
-
- val name: String?
- get() = nonSelfMembers
- .mapNotNull { it.identity?.username }
- .joinToString()
- .takeIf { it.isNotEmpty() }
-
- val nonSelfMembers: List
- get() = members.filterNot { it.isSelf }
-
- override fun toString(): String {
- return """
- {
- id:${idBase58},
- title:$title,
- hasRevealedIdentity:$hasRevealedIdentity,
- members:${members.joinToString()}
- }
- """.trimIndent()
- }
-}
-
-@Serializable
-@Entity(tableName = "conversation_pointers", primaryKeys = ["conversationIdBase58", "status"])
-data class ConversationPointerCrossRef(
- val conversationIdBase58: String,
- val messageIdString: String,
- val status: MessageStatus,
-) {
- @Ignore
- val conversationId: ID = Base58.decode(conversationIdBase58).toList()
-
- @Ignore
- @Transient
- val messageId: UUID = UUID.fromString(messageIdString)
-}
-
-@Serializable
-data class ConversationWithLastPointers(
- @Embedded val conversation: Conversation,
- @Relation(
- parentColumn = "idBase58",
- entityColumn = "conversationIdBase58",
- entity = ConversationPointerCrossRef::class,
- )
- val pointersCrossRef: List
-) {
- val pointers: Map
- get() {
- return pointersCrossRef
- .associateBy { it.status }
- .mapKeys { it.value.messageId }
- .mapValues { it.value.status }
- }
-}
-
-@Serializable
-@Entity(tableName = "messages")
-data class ConversationMessage(
- @PrimaryKey
- val idBase58: String,
- val cursorBase58: String,
- val conversationIdBase58: String,
- val dateMillis: Long,
-) {
- @Ignore
- val id: ID = Base58.decode(idBase58).toList()
-
- @Ignore
- val conversationId: ID = Base58.decode(conversationIdBase58).toList()
-
- @Ignore
- val cursor: Cursor = Base58.decode(cursorBase58).toList()
-}
-
-@Serializable
-@Entity(tableName = "message_contents", primaryKeys = ["messageIdBase58", "content"])
-data class ConversationMessageContent(
- val messageIdBase58: String,
- val content: MessageContent
-)
-
-data class ConversationMessageWithContent(
- @Embedded val message: ConversationMessage,
- @Relation(
- parentColumn = "idBase58",
- entityColumn = "messageIdBase58",
- entity = ConversationMessageContent::class,
- projection = ["content"]
- )
- val contents: List,
-)
-
-@Entity(tableName = "conversation_intent_id_mapping")
-data class ConversationIntentIdReference(
- @PrimaryKey
- val conversationIdBase58: String,
- val intentIdBase58: String,
-) {
- @Ignore
- val conversationId: ID = Base58.decode(conversationIdBase58).toList()
-
- @Ignore
- val intentId: ID = Base58.decode(intentIdBase58).toList()
-}
-
-enum class MessageStatus {
- Sent, Delivered, Read, Unknown;
-
- fun isOutgoing() = when (this) {
- Sent,
- Delivered -> true
-
- else -> false
- }
-
- fun isValid() = this != Unknown
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/Fiat.kt b/api/src/main/java/com/getcode/model/Fiat.kt
deleted file mode 100644
index fc6926abb..000000000
--- a/api/src/main/java/com/getcode/model/Fiat.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.getcode.model
-
-import kotlinx.serialization.Serializable
-
-sealed interface Value
-
-@Serializable
-data class Fiat(
- val currency: CurrencyCode,
- val amount: Double,
-): Value {
-
- companion object {
- fun fromString(currency: CurrencyCode, amountString: String): Fiat? {
- val amount = amountString.toDoubleOrNull() ?: return null
- return Fiat(
- currency = currency,
- amount = amount
- )
- }
- }
-}
-
-sealed interface GenericAmount {
-
- val currencyCode: CurrencyCode
- data class Exact(val amount: KinAmount): GenericAmount {
- override val currencyCode: CurrencyCode = amount.rate.currency
- }
- data class Partial(val fiat: Fiat): GenericAmount {
- override val currencyCode: CurrencyCode = fiat.currency
- }
-
- fun amountUsing(rate: Rate): KinAmount {
- return when (this) {
- is Exact -> amount
- is Partial -> KinAmount.fromFiatAmount(fiat.amount, rate)
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/KinAmount.kt b/api/src/main/java/com/getcode/model/KinAmount.kt
deleted file mode 100644
index a777f7ee3..000000000
--- a/api/src/main/java/com/getcode/model/KinAmount.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package com.getcode.model
-
-import com.codeinc.gen.transaction.v2.TransactionService.ExchangeData
-import com.getcode.model.Kin.Companion.fromKin
-import com.getcode.utils.FormatUtils
-import com.getcode.utils.serializer.KinQuarksSerializer
-import com.getcode.utils.serializer.PublicKeyAsStringSerializer
-import com.getcode.utils.serializer.RateAsStringSerializer
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class KinAmount(
- @Serializable(with = KinQuarksSerializer::class)
- val kin: Kin,
- val fiat: Double,
- @Serializable(with = RateAsStringSerializer::class)
- val rate: Rate
-) {
- fun truncating() = KinAmount(
- kin = kin.toKinTruncating(),
- fiat = fiat,
- rate = rate
- )
-
- fun replacing(rate: Rate): KinAmount {
- return newInstance(this.kin, rate)
- }
-
- companion object {
- val Zero = newInstance(0, Rate.oneToOne)
-
- fun newInstance(kin: Int, rate: Rate): KinAmount {
- return newInstance(fromKin(kin), rate)
- }
-
- fun newInstance(kin: Kin, rate: Rate): KinAmount {
- return KinAmount(
- kin = kin,
- fiat = kin.toFiat(fx = rate.fx),
- rate = rate
- )
- }
-
- fun fromFiatAmount(kin: Kin, fiat: Double, fx: Double, currencyCode: CurrencyCode): KinAmount {
- return KinAmount(
- kin = kin.inflating(),
- fiat = fiat,
- rate = Rate(
- fx = fx,
- currency = currencyCode
- )
- )
- }
-
- fun fromFiatAmount(fiat: Double, fx: Double, currencyCode: CurrencyCode): KinAmount {
- return fromFiatAmount(
- kin = Kin.fromFiat(fiat = fiat, fx = fx),
- fiat = fiat,
- fx = fx,
- currencyCode = currencyCode
- )
- }
-
- fun fromFiatAmount(fiat: Double, rate: Rate): KinAmount {
- return fromFiatAmount(
- kin = Kin.fromFiat(fiat = fiat, fx = rate.fx),
- fiat = fiat,
- fx = rate.fx,
- currencyCode = rate.currency
- )
- }
-
- fun fromFiatAmount(fiat: Fiat, rate: Rate): KinAmount {
- return KinAmount(
- kin = Kin.fromFiat(fiat = fiat.amount, fx = rate.fx),
- fiat = fiat.amount,
- rate = rate
- )
- }
-
- fun fromProtoExchangeData(exchangeData: ExchangeData): KinAmount {
- return fromFiatAmount(
- kin = Kin(exchangeData.quarks),
- fiat = exchangeData.nativeAmount,
- fx = exchangeData.exchangeRate,
- currencyCode = CurrencyCode.tryValueOf(exchangeData.currency)!!
- )
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/TwitterUser.kt b/api/src/main/java/com/getcode/model/TwitterUser.kt
deleted file mode 100644
index 6d0318d9d..000000000
--- a/api/src/main/java/com/getcode/model/TwitterUser.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.getcode.model
-
-import android.webkit.MimeTypeMap
-import com.codeinc.gen.user.v1.IdentityService
-import com.codeinc.gen.user.v1.friendshipCostOrNull
-import com.getcode.solana.keys.PublicKey
-import com.getcode.utils.serializer.PublicKeyAsStringSerializer
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class TwitterUser(
- override val username: String,
- @Serializable(with = PublicKeyAsStringSerializer::class)
- override val tipAddress: PublicKey,
- override val imageUrl: String?,
- val displayName: String,
- val followerCount: Int,
- val verificationStatus: VerificationStatus,
- override val costOfFriendship: Fiat,
- override val isFriend: Boolean,
- override val chatId: ID,
-): SocialUser {
-
- override val platform: String = "X"
-
- override val imageUrlSanitized: String?
- get() {
- val url = imageUrl ?: return null
- val extension = MimeTypeMap.getFileExtensionFromUrl(url)
- val urlWithoutExtension = url.removeSuffix(extension)
- val urlWithoutType = urlWithoutExtension.substringBeforeLast("_")
-
- return urlWithoutType.plus(".$extension")
- }
-
- enum class VerificationStatus {
- none, blue, business, government, unknown
- }
-
- companion object {
- fun invoke(proto: IdentityService.TwitterUser): TwitterUser? {
- val avatarUrl = proto.profilePicUrl
-
- val tipAddress = runCatching { PublicKey.fromByteString(proto.tipAddress.value) }.getOrNull() ?: return null
-
- return TwitterUser(
- username = proto.username,
- displayName = proto.name,
- imageUrl = avatarUrl,
- followerCount = proto.followerCount,
- tipAddress = tipAddress,
- verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown,
- costOfFriendship = proto.friendshipCostOrNull?.let {
- val currency = CurrencyCode.tryValueOf(it.currency) ?: return@let null
- Fiat(currency, it.nativeAmount)
- } ?: Fiat(currency = CurrencyCode.USD, amount = 1.00),
- isFriend = runCatching { proto.isFriend }.getOrNull() ?: false,
- chatId = proto.friendChatId.value.toList()
- )
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/Username.kt b/api/src/main/java/com/getcode/model/Username.kt
deleted file mode 100644
index 32a3b81eb..000000000
--- a/api/src/main/java/com/getcode/model/Username.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.getcode.model
-
-data class Username(val value: String): Value
\ 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
deleted file mode 100644
index 21e624b66..000000000
--- a/api/src/main/java/com/getcode/model/chat/Chat.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package com.getcode.model.chat
-
-import com.getcode.model.Cursor
-import com.getcode.model.ID
-import kotlinx.serialization.Serializable
-import java.util.UUID
-
-/**
- * Chat domain model for On-Chain messaging. This serves as a reference to a collection of messages.
- *
- * @param id Unique chat identifier ([ID])
- * @param type The type of chat
- * @param title The chat title, which will be localized by server when applicable
- * @param members The members in this chat
- * For [ChatType.Notification], this list has exactly 1 item
- * For [ChatType.TwoWay], this list has exactly 2 items
- * @param canMute Can the user mute this chat?
- * @param canUnsubscribe Can the user unsubscribe from this chat?
- * @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,
- val title: Title?,
- val members: List = emptyList(),
- private val _unreadCount: Int = 0,
- private val _isMuted: Boolean = false,
- val canMute: Boolean,
- private val _isSubscribed: Boolean = false,
- val canUnsubscribe: Boolean,
- val cursor: Cursor,
- val messages: List
-) {
- val imageData: Any?
- get() {
- return when (type) {
- ChatType.Unknown -> id
- ChatType.Notification -> id
- ChatType.TwoWay -> {
- members
- .filterNot { it.isSelf }
- .firstNotNullOfOrNull {
- if (it.identity != null) {
- it.identity.imageUrl.orEmpty()
- } else {
- it.id
- }
- }
- }
- }
- }
-
- val unreadCount: Int
- get() {
- if (!isV2) return _unreadCount
-
- val self = members.firstOrNull { it.isSelf } ?: return 0
- return self.numUnread
- }
-
- fun resetUnreadCount(): Chat {
- if (!isV2) {
- return copy(_unreadCount = 0)
- }
-
- val self = members.firstOrNull { it.isSelf } ?: return this
- val updatedSelf = self.copy(numUnread = 0)
- val updatedMembers = members.map {
- if (it.id == self.id) {
- updatedSelf
- } else {
- it
- }
- }
- return copy(members = updatedMembers)
- }
-
- val isMuted: Boolean
- get() {
- if (!isV2) return _isMuted
-
- val self = members.firstOrNull { it.isSelf } ?: return false
- return self.isMuted
- }
-
- fun setMuteState(muted: Boolean): Chat {
- if (!isV2) {
- return copy(_isMuted = muted)
- }
-
- val self = members.firstOrNull { it.isSelf } ?: return this
- val updatedSelf = self.copy(isMuted = muted)
- val updatedMembers = members.map {
- if (it.id == self.id) {
- updatedSelf
- } else {
- it
- }
- }
- return copy(members = updatedMembers)
- }
-
- val isSubscribed: Boolean
- get() {
- if (!isV2) return _isSubscribed
-
- val self = members.firstOrNull { it.isSelf } ?: return false
- return self.isSubscribed
- }
-
- fun setSubscriptionState(subscribed: Boolean): Chat {
- if (!isV2) {
- return copy(_isSubscribed = subscribed)
- }
-
- val self = members.firstOrNull { it.isSelf } ?: return this
- val updatedSelf = self.copy(isSubscribed = subscribed)
- val updatedMembers = members.map {
- if (it.id == self.id) {
- updatedSelf
- } else {
- it
- }
- }
- return copy(members = updatedMembers)
- }
-
- val newestMessage: ChatMessage?
- get() = messages.maxByOrNull { it.dateMillis }
-
- val lastMessageMillis: Long?
- get() = newestMessage?.dateMillis
-}
-
-val Chat.isV2: Boolean
- get() = members.isNotEmpty()
-
-val Chat.isNotification: Boolean
- get() = type == ChatType.Notification
-
-val Chat.isConversation: Boolean
- get() = type == ChatType.TwoWay
-
-val Chat.self: ChatMember?
- get() = members.firstOrNull { it.isSelf }
-
-val Chat.selfId: UUID?
- get() = self?.id
diff --git a/api/src/main/java/com/getcode/model/chat/ChatMember.kt b/api/src/main/java/com/getcode/model/chat/ChatMember.kt
deleted file mode 100644
index d49792f80..000000000
--- a/api/src/main/java/com/getcode/model/chat/ChatMember.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.getcode.model.chat
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.model.ID
-import com.getcode.utils.serializer.UUIDSerializer
-import kotlinx.serialization.Contextual
-import kotlinx.serialization.Serializable
-import java.util.UUID
-
-/**
- * A user in a chat
- *
- * @param id Globally unique ID for this chat member
- * @param isSelf Is this chat member yourself? This enables client to identify which member_id
- * is themselves.
- * @param identity The chat member's identity if it has been revealed.
- * @param pointers Chat message state for this member. This list will have DELIVERED and READ
- * pointers, if they exist. SENT pointers should be inferred by persistence
- * on server.
- * @param numUnread Estimated number of unread messages for the chat member in this chat
- * Only valid when `isSelf = true`
- * @param isMuted Has the chat member muted this chat?
- * Only valid when `isSelf = true`
- * @param isSubscribed Is the chat member subscribed to this chat?
- * Only valid when `isSelf = true`
- */
-@Serializable
-data class ChatMember(
- @Serializable(with = UUIDSerializer::class)
- val id: UUID,
- val isSelf: Boolean,
- val identity: Identity?,
- val pointers: List,
- val numUnread: Int,
- val isMuted: Boolean,
- val isSubscribed: Boolean,
-)
-
-/**
- * Identity to an external social platform that can be linked to a Code account
- *
- * @param platform The external social platform linked to this chat member
- * @param username The chat member's username on the external social platform
- */
-@Serializable
-data class Identity(
- val platform: Platform,
- val username: String,
- val imageUrl: String?
-) {
- companion object {
- operator fun invoke(proto: ChatService.ChatMemberIdentity): Identity? {
- val platform = Platform(proto.platform).takeIf { it != Platform.Unknown } ?: return null
- return Identity(
- platform = platform,
- username = proto.username,
- imageUrl = null,
- )
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/ChatMessage.kt b/api/src/main/java/com/getcode/model/chat/ChatMessage.kt
deleted file mode 100644
index cf4660aa4..000000000
--- a/api/src/main/java/com/getcode/model/chat/ChatMessage.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.getcode.model.chat
-
-import com.getcode.ed25519.Ed25519.KeyPair
-import com.getcode.model.Cursor
-import com.getcode.model.ID
-import com.getcode.utils.serializer.UUIDSerializer
-import kotlinx.serialization.Serializable
-import java.util.UUID
-
-/**
- * A message in a chat
- *
- * @param id Globally unique ID for this message
- * This is a time based UUID in v2
- * @param senderId The chat member that sent the message.
- * For [ChatType.Notification] chats, this field is omitted since the chat has exactly 1 member.
- * @param cursor Cursor value for this message for reference in a paged GetMessagesRequest
- * @param dateMillis Timestamp this message was generated at
- * @param contents Ordered message content. A message may have more than one piece of content.
- */
-@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,
- val dateMillis: Long,
- val contents: List,
-) {
- val hasEncryptedContent: Boolean
- get() {
- return contents.firstOrNull { it is MessageContent.SodiumBox } != null
- }
-
- fun decryptingUsing(keyPair: KeyPair): ChatMessage {
- return ChatMessage(
- id = id,
- senderId = senderId,
- isFromSelf = isFromSelf,
- dateMillis = dateMillis,
- cursor = cursor,
- contents = contents.map {
- when (it) {
- is MessageContent.Exchange,
- is MessageContent.Localized,
- is MessageContent.Decrypted,
- is MessageContent.IdentityRevealed,
- is MessageContent.RawText,
- is MessageContent.ThankYou -> it // passthrough
- is MessageContent.SodiumBox -> {
- val decrypted = it.data.decryptMessageUsingNaClBox(keyPair = keyPair)
- if (decrypted != null) {
- MessageContent.Decrypted(data = decrypted, isFromSelf = isFromSelf)
- } else {
- it
- }
- }
-
-
- }
- }
- )
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/ChatStreamEventUpdate.kt b/api/src/main/java/com/getcode/model/chat/ChatStreamEventUpdate.kt
deleted file mode 100644
index e0e1612c9..000000000
--- a/api/src/main/java/com/getcode/model/chat/ChatStreamEventUpdate.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.getcode.model.chat
-
-import com.getcode.mapper.PointerStatus
-
-data class ChatStreamEventUpdate(
- val messages: List,
- val pointers: List,
- val isTyping: Boolean,
-)
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/ChatType.kt b/api/src/main/java/com/getcode/model/chat/ChatType.kt
deleted file mode 100644
index 72ea2119d..000000000
--- a/api/src/main/java/com/getcode/model/chat/ChatType.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.getcode.model.chat
-
-import com.codeinc.gen.chat.v2.ChatService
-
-enum class ChatType {
- Unknown,
- Notification,
- TwoWay;
-
- companion object {
- operator fun invoke(proto: ChatService.ChatType): ChatType {
- return runCatching { entries[proto.ordinal] }.getOrNull() ?: Unknown
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/Chats.kt b/api/src/main/java/com/getcode/model/chat/Chats.kt
deleted file mode 100644
index eab91b077..000000000
--- a/api/src/main/java/com/getcode/model/chat/Chats.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.getcode.model.chat
-
-typealias ModelIntentId = com.codeinc.gen.common.v1.Model.IntentId
-
-typealias ChatGrpcV1 = com.codeinc.gen.chat.v1.ChatGrpc
-typealias ChatGrpcV2 = com.codeinc.gen.chat.v2.ChatGrpc
-
-typealias ChatIdV1 = com.codeinc.gen.chat.v1.ChatService.ChatId
-typealias ChatIdV2 = com.codeinc.gen.common.v1.Model.ChatId
-
-typealias MessageContentV1 = com.codeinc.gen.chat.v1.ChatService.Content
-typealias MessageContentV2 = com.codeinc.gen.chat.v2.ChatService.Content
-
-typealias VerbV1 = com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb
-typealias VerbV2 = com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent.Verb
-
-typealias ChatCursorV1 = com.codeinc.gen.chat.v1.ChatService.Cursor
-typealias ChatCursorV2 = com.codeinc.gen.chat.v2.ChatService.Cursor
-
-typealias GetMessagesDirectionV1 = com.codeinc.gen.chat.v1.ChatService.GetMessagesRequest.Direction
-typealias GetMessagesDirectionV2 = com.codeinc.gen.chat.v2.ChatService.GetMessagesRequest.Direction
-
-typealias PointerV1 = com.codeinc.gen.chat.v1.ChatService.Pointer
-typealias PointerV2 = com.codeinc.gen.chat.v2.ChatService.Pointer
-
-typealias StartChatRequest = com.codeinc.gen.chat.v2.ChatService.StartChatRequest
-typealias StartChatResponse = com.codeinc.gen.chat.v2.ChatService.StartChatResponse
-
-typealias GetChatsRequestV1 = com.codeinc.gen.chat.v1.ChatService.GetChatsRequest
-typealias GetChatsRequestV2 = com.codeinc.gen.chat.v2.ChatService.GetChatsRequest
-typealias GetChatsResponseV1 = com.codeinc.gen.chat.v1.ChatService.GetChatsResponse
-typealias GetChatsResponseV2 = com.codeinc.gen.chat.v2.ChatService.GetChatsResponse
-
-typealias GetMessagesRequestV1 = com.codeinc.gen.chat.v1.ChatService.GetMessagesRequest
-typealias GetMessagesRequestV2 = com.codeinc.gen.chat.v2.ChatService.GetMessagesRequest
-typealias GetMessagesResponseV1 = com.codeinc.gen.chat.v1.ChatService.GetMessagesResponse
-typealias GetMessagesResponseV2 = com.codeinc.gen.chat.v2.ChatService.GetMessagesResponse
-
-typealias AdvancePointerRequestV1 = com.codeinc.gen.chat.v1.ChatService.AdvancePointerRequest
-typealias AdvancePointerRequestV2 = com.codeinc.gen.chat.v2.ChatService.AdvancePointerRequest
-typealias AdvancePointerResponseV1 = com.codeinc.gen.chat.v1.ChatService.AdvancePointerResponse
-typealias AdvancePointerResponseV2 = com.codeinc.gen.chat.v2.ChatService.AdvancePointerResponse
-
-
-typealias SetMuteStateRequestV1 = com.codeinc.gen.chat.v1.ChatService.SetMuteStateRequest
-typealias SetMuteStateRequestV2 = com.codeinc.gen.chat.v2.ChatService.SetMuteStateRequest
-typealias SetMuteStateResponseV1 = com.codeinc.gen.chat.v1.ChatService.SetMuteStateResponse
-typealias SetMuteStateResponseV2 = com.codeinc.gen.chat.v2.ChatService.SetMuteStateResponse
-
-
-typealias SetSubscriptionStateRequestV1 = com.codeinc.gen.chat.v1.ChatService.SetSubscriptionStateRequest
-typealias SetSubscriptionStateRequestV2 = com.codeinc.gen.chat.v2.ChatService.SetSubscriptionStateRequest
-typealias SetSubscriptionStateResponseV1 = com.codeinc.gen.chat.v1.ChatService.SetSubscriptionStateResponse
-typealias SetSubscriptionStateResponseV2 = com.codeinc.gen.chat.v2.ChatService.SetSubscriptionStateResponse
-
-/**
- * Code reference to a V1 [Chat] that serves as a collection of messages associated
- * with a notification type (Tips, Cash Payments, Web Payments, etc.)
- */
-typealias NotificationCollectionEntity = Chat
-
-/**
- * Code reference to a V2 [Chat] that is a full end-to-end chat that suports
- * peer-to-peer messaging between users.
- */
-typealias ConversationEntity = Chat
diff --git a/api/src/main/java/com/getcode/model/chat/MessageContent.kt b/api/src/main/java/com/getcode/model/chat/MessageContent.kt
deleted file mode 100644
index d9dd7e054..000000000
--- a/api/src/main/java/com/getcode/model/chat/MessageContent.kt
+++ /dev/null
@@ -1,413 +0,0 @@
-package com.getcode.model.chat
-
-import com.codeinc.gen.chat.v1.ChatService as ChatServiceV1
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.model.CurrencyCode
-import com.getcode.model.EncryptedData
-import com.getcode.model.Fiat
-import com.getcode.model.GenericAmount
-import com.getcode.model.ID
-import com.getcode.model.Kin
-import com.getcode.model.KinAmount
-import com.getcode.model.Rate
-import com.getcode.network.repository.toPublicKey
-import com.getcode.utils.serializer.MessageContentSerializer
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-
-@Serializable
-sealed interface MessageContent {
- val kind: Int
- val isFromSelf: Boolean
-
- @Serializable
- data class Localized(
- val value: String,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 0
-
- override fun hashCode(): Int {
- var result = value.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Localized
-
- if (value != other.value) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class RawText(
- val value: String,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 1
-
- override fun hashCode(): Int {
- var result = value.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as RawText
-
- if (value != other.value) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class Exchange(
- val amount: GenericAmount,
- val verb: Verb,
- val reference: Reference,
- val hasInteracted: Boolean,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 2
-
- override fun hashCode(): Int {
- var result = amount.hashCode()
- result += verb.hashCode()
- result += reference.hashCode()
- result += hasInteracted.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Exchange
-
- if (amount != other.amount) return false
- if (verb != other.verb) return false
- if (reference != other.reference) return false
- if (hasInteracted != other.hasInteracted) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class SodiumBox(
- val data: EncryptedData,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 3
-
- override fun hashCode(): Int {
- var result = data.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as SodiumBox
-
- if (data != other.data) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class ThankYou(
- val tipIntentId: ID,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 4
-
- override fun hashCode(): Int {
- var result = tipIntentId.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as ThankYou
-
- if (tipIntentId != other.tipIntentId) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class IdentityRevealed(
- val memberId: ID,
- val identity: Identity,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 5
-
- override fun hashCode(): Int {
- var result = memberId.hashCode()
- result += identity.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as IdentityRevealed
-
- if (memberId != other.memberId) return false
- if (identity != other.identity) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- @Serializable
- data class Decrypted(
- val data: String,
- override val isFromSelf: Boolean,
- ) : MessageContent {
- override val kind: Int = 6
-
- override fun hashCode(): Int {
- var result = data.hashCode()
- result += isFromSelf.hashCode()
- result += kind.hashCode()
-
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Decrypted
-
- if (data != other.data) return false
- if (isFromSelf != other.isFromSelf) return false
- if (kind != other.kind) return false
-
- return true
- }
- }
-
- companion object {
- operator fun invoke(
- proto: MessageContentV2,
- isFromSelf: Boolean = false,
- ): MessageContent? {
- return when (proto.typeCase) {
- ChatService.Content.TypeCase.LOCALIZED -> Localized(
- isFromSelf = isFromSelf,
- value = proto.localized.keyOrText
- )
-
- ChatService.Content.TypeCase.EXCHANGE_DATA -> {
- val verb = Verb(proto.exchangeData.verb)
- when (proto.exchangeData.exchangeDataCase) {
- ChatService.ExchangeDataContent.ExchangeDataCase.EXACT -> {
- val exact = proto.exchangeData.exact
- val currency = CurrencyCode.tryValueOf(exact.currency) ?: return null
- val kinAmount = KinAmount.newInstance(
- kin = Kin.fromQuarks(exact.quarks),
- rate = Rate(
- fx = exact.exchangeRate,
- currency = currency
- )
- )
-
-
- val reference = Reference(proto.exchangeData)
- Exchange(
- isFromSelf = isFromSelf,
- amount = GenericAmount.Exact(kinAmount),
- verb = verb,
- reference = reference,
- hasInteracted = false,
- )
- }
-
- ChatService.ExchangeDataContent.ExchangeDataCase.PARTIAL -> {
- val partial = proto.exchangeData.partial
- val currency = CurrencyCode.tryValueOf(partial.currency) ?: return null
-
- val fiat = Fiat(
- currency = currency,
- amount = partial.nativeAmount
- )
-
- val reference = Reference(proto.exchangeData)
-
- Exchange(
- isFromSelf = isFromSelf,
- amount = GenericAmount.Partial(fiat),
- verb = verb,
- reference = reference,
- hasInteracted = false
- )
- }
-
- ChatService.ExchangeDataContent.ExchangeDataCase.EXCHANGEDATA_NOT_SET -> return null
- else -> return null
- }
- }
-
- ChatService.Content.TypeCase.NACL_BOX -> {
- val encryptedContent = proto.naclBox
- val peerPublicKey =
- encryptedContent.peerPublicKey.value.toByteArray().toPublicKey()
-
- val data = EncryptedData(
- peerPublicKey = peerPublicKey,
- nonce = encryptedContent.nonce.toByteArray().toList(),
- encryptedData = encryptedContent.encryptedPayload.toByteArray().toList(),
- )
- SodiumBox(isFromSelf = isFromSelf, data = data)
- }
-
- ChatService.Content.TypeCase.THANK_YOU -> {
- ThankYou(
- isFromSelf = isFromSelf,
- tipIntentId = proto.thankYou.tipIntent.value.toByteArray().toList()
- )
- }
-
- ChatService.Content.TypeCase.IDENTITY_REVEALED -> {
- IdentityRevealed(
- isFromSelf = isFromSelf,
- memberId = proto.identityRevealed.memberId.value.toByteArray().toList(),
- identity = Identity(proto.identityRevealed.identity) ?: return null
- )
- }
-
- ChatService.Content.TypeCase.TEXT -> RawText(
- isFromSelf = isFromSelf,
- value = proto.text.text
- )
-
- ChatService.Content.TypeCase.TYPE_NOT_SET -> return null
- else -> return null
- }
- }
-
- fun fromV1(
- messageId: ID,
- proto: MessageContentV1,
- ): MessageContent? {
- return when (proto.typeCase) {
- ChatServiceV1.Content.TypeCase.SERVER_LOCALIZED -> Localized(
- isFromSelf = false,
- value = proto.serverLocalized.keyOrText
- )
-
- ChatServiceV1.Content.TypeCase.EXCHANGE_DATA -> {
- val verb = Verb.fromV1(proto.exchangeData.verb)
- val isFromSelf = !verb.increasesBalance
- when (proto.exchangeData.exchangeDataCase) {
- ChatServiceV1.ExchangeDataContent.ExchangeDataCase.EXACT -> {
- val exact = proto.exchangeData.exact
- val currency = CurrencyCode.tryValueOf(exact.currency) ?: return null
- val kinAmount = KinAmount.newInstance(
- kin = Kin.fromQuarks(exact.quarks),
- rate = Rate(
- fx = exact.exchangeRate,
- currency = currency
- )
- )
-
- Exchange(
- isFromSelf = isFromSelf,
- amount = GenericAmount.Exact(kinAmount),
- verb = verb,
- reference = Reference.IntentId(messageId),
- hasInteracted = false,
- )
- }
-
- ChatServiceV1.ExchangeDataContent.ExchangeDataCase.PARTIAL -> {
- val partial = proto.exchangeData.partial
- val currency = CurrencyCode.tryValueOf(partial.currency) ?: return null
-
- val fiat = Fiat(
- currency = currency,
- amount = partial.nativeAmount
- )
-
- Exchange(
- isFromSelf = isFromSelf,
- amount = GenericAmount.Partial(fiat),
- verb = verb,
- reference = Reference.IntentId(messageId),
- hasInteracted = false
- )
- }
-
- ChatServiceV1.ExchangeDataContent.ExchangeDataCase.EXCHANGEDATA_NOT_SET -> return null
- else -> return null
- }
- }
-
- ChatServiceV1.Content.TypeCase.NACL_BOX -> {
- val encryptedContent = proto.naclBox
- val peerPublicKey =
- encryptedContent.peerPublicKey.value.toByteArray().toPublicKey()
-
- val data = EncryptedData(
- peerPublicKey = peerPublicKey,
- nonce = encryptedContent.nonce.toByteArray().toList(),
- encryptedData = encryptedContent.encryptedPayload.toByteArray().toList(),
- )
- SodiumBox(isFromSelf = false, data = data)
- }
-
- ChatServiceV1.Content.TypeCase.TYPE_NOT_SET -> return null
- else -> return null
- }
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/OutgoingMessageContent.kt b/api/src/main/java/com/getcode/model/chat/OutgoingMessageContent.kt
deleted file mode 100644
index fc6552794..000000000
--- a/api/src/main/java/com/getcode/model/chat/OutgoingMessageContent.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.getcode.model.chat
-
-import com.getcode.model.ID
-
-sealed interface OutgoingMessageContent {
- data class Text(val text: String): OutgoingMessageContent
- data class ThankYou(val tipIntentId: ID): OutgoingMessageContent
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/model/chat/Reference.kt b/api/src/main/java/com/getcode/model/chat/Reference.kt
deleted file mode 100644
index b36d563e4..000000000
--- a/api/src/main/java/com/getcode/model/chat/Reference.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.getcode.model.chat
-
-import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent
-import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent.ReferenceCase
-import com.getcode.model.ID
-import com.getcode.solana.keys.Signature
-
-/**
- * An ID that can be referenced to the source of the exchange of Kin
- */
-sealed interface Reference {
- data object NoneSet: Reference
- data class IntentId(val id: ID): Reference
- data class Signature(val signature: com.getcode.solana.keys.Signature): Reference
-
- companion object {
- operator fun invoke(proto: ExchangeDataContent): Reference {
- return when (proto.referenceCase) {
- ReferenceCase.INTENT -> IntentId(proto.intent.toByteArray().toList())
- ReferenceCase.SIGNATURE -> Signature(Signature(proto.signature.toByteArray().toList()))
- ReferenceCase.REFERENCE_NOT_SET -> NoneSet
- null -> NoneSet
- }
- }
- }
-}
diff --git a/api/src/main/java/com/getcode/model/chat/Verb.kt b/api/src/main/java/com/getcode/model/chat/Verb.kt
deleted file mode 100644
index 383106dd1..000000000
--- a/api/src/main/java/com/getcode/model/chat/Verb.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.getcode.model.chat
-
-import com.codeinc.gen.chat.v1.ChatService as ChatServiceV1
-import com.codeinc.gen.chat.v2.ChatService
-
-sealed interface Verb {
- val increasesBalance: Boolean
-
- data object Unknown : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Gave : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Received : Verb {
- override val increasesBalance: Boolean = true
- }
-
- data object Withdrew : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Deposited : Verb {
- override val increasesBalance: Boolean = true
- }
-
- data object Sent : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Returned : Verb {
- override val increasesBalance: Boolean = true
- }
-
- data object Spent : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Paid : Verb {
- override val increasesBalance: Boolean = false
- }
-
- data object Purchased : Verb {
- override val increasesBalance: Boolean = true
- }
-
- data object ReceivedTip : Verb {
- override val increasesBalance: Boolean = true
- }
-
- data object SentTip : Verb {
- override val increasesBalance: Boolean = false
- }
-
- companion object {
- operator fun invoke(proto: VerbV2): Verb {
- return when (proto) {
- ChatService.ExchangeDataContent.Verb.UNKNOWN -> Unknown
- ChatService.ExchangeDataContent.Verb.GAVE -> Gave
- ChatService.ExchangeDataContent.Verb.RECEIVED -> Received
- ChatService.ExchangeDataContent.Verb.WITHDREW -> Withdrew
- ChatService.ExchangeDataContent.Verb.DEPOSITED -> Deposited
- ChatService.ExchangeDataContent.Verb.SENT -> Sent
- ChatService.ExchangeDataContent.Verb.RETURNED -> Returned
- ChatService.ExchangeDataContent.Verb.SPENT -> Spent
- ChatService.ExchangeDataContent.Verb.PAID -> Paid
- ChatService.ExchangeDataContent.Verb.PURCHASED -> Purchased
- ChatService.ExchangeDataContent.Verb.UNRECOGNIZED -> Unknown
- ChatService.ExchangeDataContent.Verb.RECEIVED_TIP -> ReceivedTip
- ChatService.ExchangeDataContent.Verb.SENT_TIP -> SentTip
- }
- }
-
- fun fromV1(proto: VerbV1): Verb {
- return when (proto) {
- ChatServiceV1.ExchangeDataContent.Verb.UNKNOWN -> Unknown
- ChatServiceV1.ExchangeDataContent.Verb.GAVE -> Gave
- ChatServiceV1.ExchangeDataContent.Verb.RECEIVED -> Received
- ChatServiceV1.ExchangeDataContent.Verb.WITHDREW -> Withdrew
- ChatServiceV1.ExchangeDataContent.Verb.DEPOSITED -> Deposited
- ChatServiceV1.ExchangeDataContent.Verb.SENT -> Sent
- ChatServiceV1.ExchangeDataContent.Verb.RETURNED -> Returned
- ChatServiceV1.ExchangeDataContent.Verb.SPENT -> Spent
- ChatServiceV1.ExchangeDataContent.Verb.PAID -> Paid
- ChatServiceV1.ExchangeDataContent.Verb.PURCHASED -> Purchased
- ChatServiceV1.ExchangeDataContent.Verb.UNRECOGNIZED -> Unknown
- ChatServiceV1.ExchangeDataContent.Verb.RECEIVED_TIP -> ReceivedTip
- ChatServiceV1.ExchangeDataContent.Verb.SENT_TIP -> SentTip
- }
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/network/ChatHistoryController.kt b/api/src/main/java/com/getcode/network/ChatHistoryController.kt
deleted file mode 100644
index 6cd51243e..000000000
--- a/api/src/main/java/com/getcode/network/ChatHistoryController.kt
+++ /dev/null
@@ -1,221 +0,0 @@
-package com.getcode.network
-
-import androidx.paging.PagingData
-import androidx.paging.PagingSource
-import com.getcode.db.AppDatabase
-import com.getcode.db.Database
-import com.getcode.ed25519.Ed25519.KeyPair
-import com.getcode.manager.SessionManager
-import com.getcode.mapper.ConversationMapper
-import com.getcode.mapper.ConversationMessageMapper
-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.ChatMessage
-import com.getcode.model.chat.ConversationEntity
-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.selfId
-import com.getcode.network.client.Client
-import com.getcode.network.client.advancePointer
-import com.getcode.network.client.fetchMessagesFor
-import com.getcode.network.client.fetchV2Chats
-import com.getcode.network.repository.encodeBase64
-import com.getcode.utils.TraceType
-import com.getcode.utils.trace
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class ChatHistoryController @Inject constructor(
- private val client: Client,
- private val twitterUserController: TwitterUserController,
- private val conversationMapper: ConversationMapper,
- private val conversationMessageMapper: ConversationMessageMapper,
-) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
-
- private val chatEntries = MutableStateFlow?>(null)
-
- val chats: StateFlow?>
- get() = chatEntries
- .map { it?.filter { entry -> entry.isConversation } }
- .stateIn(this, SharingStarted.Eagerly, emptyList())
-
- var loadingMessages: Boolean = false
-
- private val db: AppDatabase by lazy { Database.requireInstance() }
-
- private val pagerMap = mutableMapOf>()
- private val chatFlows = mutableMapOf>>()
-
- fun reset() {
- pagerMap.clear()
- chatFlows.clear()
- }
-
- fun updateChatWithMessages(chat: ConversationEntity, messages: List) {
- val updatedMessages = (chat.messages + messages).distinctBy { it.id }
- val updatedChat = chat.copy(messages = updatedMessages)
- val chats = chatEntries.value?.map {
- if (it.id == updatedChat.id) {
- updatedChat
- } else {
- it
- }
- }?.sortedByDescending { it.lastMessageMillis }
- chatEntries.update { chats }
- }
-
- val unreadCount = chats
- .filterNotNull()
- // Ignore muted chats and unsubscribed chats
- .map { it.filter { c -> !c.isMuted && c.isSubscribed } }
- .map { it.sumOf { c -> c.unreadCount } }
-
- private fun owner(): KeyPair? = SessionManager.getKeyPair()
-
- suspend fun fetch(update: Boolean = false) {
- if (loadingMessages) return
-
- val updatedWithMessages = mutableListOf()
- val containers = fetchChatsWithoutMessages()
- trace(message = "Fetched ${containers.count()} chats", type = TraceType.Silent)
-
- if (!update) {
- pagerMap.clear()
- chatFlows.clear()
- chatEntries.value = containers
-
- loadingMessages = true
- }
-
- containers.onEach { chat ->
- val members = fetchMemberImages(chat)
- val updatedChat = chat.copy(members = members)
- val result = fetchLatestMessageForChat(updatedChat)
- result.onSuccess { message ->
- if (message != null) {
- updatedWithMessages.add(updatedChat.copy(messages = listOf(message)))
- }
- }.onFailure {
- updatedWithMessages.add(updatedChat)
- }
- }
-
- loadingMessages = false
- chatEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
- }
-
- fun addChat(chat: ConversationEntity) {
- chatEntries.value = (chatEntries.value.orEmpty() + chat)
- .sortedByDescending { it.lastMessageMillis }
- }
-
- fun findChat(predicate: (ConversationEntity) -> Boolean): ConversationEntity? {
- return chatEntries.value?.firstOrNull(predicate)
- }
-
- fun resetUnreadCount(chatId: ID) {
- chatEntries.update {
- it?.toMutableList()?.apply chats@{
- indexOfFirst { chat -> chat.id == chatId }
- .takeIf { index -> index >= 0 }
- ?.let { index ->
- val chat = this[index]
- this[index] = chat.resetUnreadCount()
- }
- }?.toList()
- }
- }
-
- private suspend fun fetchLatestMessageForChat(chat: ConversationEntity): Result {
- val encodedId = chat.id.toByteArray().encodeBase64()
- Timber.d("fetching last message for $encodedId")
- val owner = owner() ?: return Result.success(null)
- return client.fetchMessagesFor(owner, chat, limit = 1)
- .onSuccess { result ->
- if (chat.isConversation) {
- val messages =
- result.map { message -> conversationMessageMapper.map(chat.id to message) }
- val memberId = chat.selfId ?: return@onSuccess
- val latestRef = messages.maxBy { it.dateMillis }
- client.advancePointer(
- owner,
- chat,
- latestRef.id,
- memberId,
- MessageStatus.Delivered
- )
- }
- }
- .onFailure {
- Timber.e(t = it, "Failed to fetch messages for $encodedId.")
- }.map { it.getOrNull(0) }
- }
-
- private suspend fun fetchChatsWithoutMessages(): List {
- val owner = owner() ?: return emptyList()
- val result = client.fetchV2Chats(owner)
- .map { chats ->
- chats.map { chat ->
- // map revealed identity as title if known
- if (chat.isConversation) {
- val conversation = conversationMapper.map(chat)
- conversation.name?.let { chat.copy(title = Title.Localized(it)) } ?: chat
- } else {
- chat
- }
- }
- }
- .onSuccess { result ->
- result.filter { it.isConversation }
- .let { chats ->
- val chatIds = chats.map { it.id }
- db.conversationDao().purgeConversationsNotIn(chatIds)
- db.conversationMessageDao().purgeMessagesNotIn(chatIds)
- db.conversationPointersDao().purgePointersNoLongerNeeded(chatIds)
- db.conversationIntentMappingDao().purgeMappingNoLongerNeeded(chatIds)
- chats
- }
- .onEach {
- val conversation = conversationMapper.map(it)
- db.conversationDao().upsertConversations(conversation)
- }
- }
- return result.getOrNull().orEmpty()
- }
-
- private suspend fun fetchMemberImages(chat: ConversationEntity): List {
- return chat.members
- .map { member ->
- if (member.isSelf) return@map member
- if (member.identity == null) return@map member
- if (member.identity.imageUrl != null) return@map member
- val metadata = runCatching {
- twitterUserController.fetchUser(member.identity.username)
- }.getOrNull() ?: return@map member
-
- member.copy(
- identity = Identity(
- platform = Platform.named(metadata.platform),
- username = metadata.username,
- imageUrl = metadata.imageUrl
- )
- )
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/network/ConversationController.kt b/api/src/main/java/com/getcode/network/ConversationController.kt
deleted file mode 100644
index 01a685f7f..000000000
--- a/api/src/main/java/com/getcode/network/ConversationController.kt
+++ /dev/null
@@ -1,370 +0,0 @@
-package com.getcode.network
-
-import androidx.paging.Pager
-import androidx.paging.PagingConfig
-import androidx.paging.PagingData
-import com.getcode.api.BuildConfig
-import com.getcode.db.AppDatabase
-import com.getcode.db.Database
-import com.getcode.manager.SessionManager
-import com.getcode.mapper.ConversationMapper
-import com.getcode.mapper.ConversationMessageWithContentMapper
-import com.getcode.mapper.PointerStatus
-import com.getcode.model.Conversation
-import com.getcode.model.ConversationIntentIdReference
-import com.getcode.model.ConversationMessageWithContent
-import com.getcode.model.ConversationWithLastPointers
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.SocialUser
-import com.getcode.model.chat.ChatType
-import com.getcode.model.chat.MessageContent
-import com.getcode.model.chat.OutgoingMessageContent
-import com.getcode.model.chat.Platform
-import com.getcode.model.chat.Pointer
-import com.getcode.model.chat.isConversation
-import com.getcode.model.chat.selfId
-import com.getcode.model.description
-import com.getcode.model.uuid
-import com.getcode.network.client.ChatMessageStreamReference
-import com.getcode.network.exchange.Exchange
-import com.getcode.network.repository.base58
-import com.getcode.network.service.ChatServiceV2
-import com.getcode.utils.ErrorUtils
-import com.getcode.utils.TraceType
-import com.getcode.utils.bytes
-import com.getcode.utils.trace
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-interface ConversationController {
- fun observeConversation(id: ID): Flow
- suspend fun createConversation(identifier: ID, with: SocialUser): Conversation
- suspend fun getConversation(identifier: ID): ConversationWithLastPointers?
- suspend fun getOrCreateConversation(
- identifier: ID,
- with: SocialUser
- ): ConversationWithLastPointers
-
- fun openChatStream(scope: CoroutineScope, conversation: Conversation)
- fun closeChatStream()
- suspend fun hasInteracted(messageId: ID): Boolean
- suspend fun revealIdentity(
- conversationId: ID,
- platform: Platform,
- username: String
- ): Result
-
- suspend fun resetUnreadCount(conversationId: ID)
- suspend fun advanceReadPointer(conversationId: ID, messageId: ID, status: MessageStatus)
- suspend fun sendMessage(conversationId: ID, message: String): Result
- fun conversationPagingData(conversationId: ID): Flow>
- fun observeTyping(conversationId: ID): Flow
- suspend fun onUserStartedTypingIn(conversationId: ID)
- suspend fun onUserStoppedTypingIn(conversationId: ID)
-}
-
-class ConversationStreamController @Inject constructor(
- private val historyController: ChatHistoryController,
- private val exchange: Exchange,
- private val chatService: ChatServiceV2,
- private val conversationMapper: ConversationMapper,
- private val messageWithContentMapper: ConversationMessageWithContentMapper,
- private val tipController: TipController,
-) : ConversationController {
- private val pagingConfig = PagingConfig(pageSize = 20)
-
- private val db: AppDatabase by lazy { Database.requireInstance() }
-
- private var stream: ChatMessageStreamReference? = null
-
- private val typingChats = MutableStateFlow>(emptyList())
-
- private fun conversationPagingSource(conversationId: ID) =
- db.conversationMessageDao().observeConversationMessages(conversationId.base58)
-
- override fun observeConversation(id: ID): Flow {
- return db.conversationDao().observeConversation(id)
- }
-
- override suspend fun createConversation(identifier: ID, with: SocialUser): Conversation {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: throw IllegalStateException()
- val self = tipController.connectedAccount.value ?: throw IllegalStateException()
- return chatService.startChat(owner, self, with, identifier, ChatType.TwoWay)
- .onSuccess { historyController.addChat(it) }
- .map { conversationMapper.map(it) }
- .onSuccess {
- // TODO: remove
- // stop gap until startChat rejects created chats for the same identifier
- db.conversationIntentMappingDao()
- .insert(
- ConversationIntentIdReference(
- conversationIdBase58 = it.id.base58,
- intentIdBase58 = identifier.base58
- )
- )
- db.conversationDao().upsertConversations(it)
- }
- .getOrThrow()
- }
-
- override suspend fun getConversation(identifier: ID): ConversationWithLastPointers? {
- val conversation = db.conversationDao().findConversation(identifier) ?: return null
- return conversation
- }
-
- override suspend fun getOrCreateConversation(
- identifier: ID,
- with: SocialUser
- ): ConversationWithLastPointers {
- var conversationByChatId = getConversation(identifier)
- if (conversationByChatId != null) {
- return conversationByChatId
- }
-
- // lookup chat ID by tip intent ID
- val conversationId = db.conversationIntentMappingDao().conversationIdByReference(identifier)
- if (conversationId != null) {
- conversationByChatId = getConversation(identifier)
- }
-
- if (conversationByChatId != null) {
- return conversationByChatId
- }
-
- return ConversationWithLastPointers(createConversation(identifier, with), emptyList())
- }
-
- @Throws(IllegalStateException::class)
- override fun openChatStream(scope: CoroutineScope, conversation: Conversation) {
- runCatching { closeChatStream() }
- val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: throw IllegalStateException()
-
- val chat = historyController.findChat { it.id == conversation.id }
- ?: throw IllegalArgumentException("Unable to resolve chat for this conversation")
- val memberId = chat.selfId ?: throw IllegalStateException("Not a member of this chat")
-
- scope.launch(Dispatchers.IO) {
- savePointers(conversationId = conversation.id,
- pointers = chat.members.flatMap { member ->
- member.pointers.mapNotNull { ptr ->
- val messageId = ptr.messageId ?: return@mapNotNull null
- val uuid = ptr.memberId?.uuid ?: return@mapNotNull null
- PointerStatus(
- memberId = uuid,
- messageId = messageId,
- messageStatus = when (ptr) {
- is Pointer.Delivered -> MessageStatus.Delivered
- is Pointer.Read -> MessageStatus.Read
- is Pointer.Sent -> MessageStatus.Sent
- is Pointer.Unknown -> MessageStatus.Unknown
- }
- )
- }
- }
- )
- }
-
- stream = chatService.openChatStream(
- scope = scope,
- conversation = conversation,
- memberId = memberId,
- owner = owner,
- chatLookup = { chat }
- ) result@{ result ->
- if (result.isSuccess) {
- val updates = result.getOrNull() ?: return@result
- val (messages, pointers, isTyping) = updates
-
- typingChats.value = if (isTyping) {
- typingChats.value + listOf(conversation.id).toSet()
- } else {
- typingChats.value - listOf(conversation.id).toSet()
- }
-
- historyController.updateChatWithMessages(chat, messages)
- val messagesWithContent = messages.map {
- messageWithContentMapper.map(chat.id to it)
- }
-
- val identityRevealed = messages
- .flatMap { it.contents }
- .filterIsInstance()
- .firstOrNull()
- .takeIf { chat.isConversation }
-
- if (identityRevealed != null && conversation.members.isNotEmpty()) {
- val members = conversation.members.map {
- if (identityRevealed.memberId == it.id.bytes) {
- it.copy(identity = identityRevealed.identity)
- } else {
- it
- }
- }
- scope.launch(Dispatchers.IO) {
- db.conversationDao()
- .upsertConversations(
- conversation.copy(members = members)
- )
- }
- }
-
- trace(
- tag = "ConversationStream",
- message = "chat messages: ${messages.count()}, pointers=${pointers.count()}, isTyping=$isTyping",
- type = TraceType.Silent
- )
-
- scope.launch(Dispatchers.IO) {
- db.conversationMessageDao().upsertMessagesWithContent(messagesWithContent)
- savePointers(conversationId = conversation.id, pointers = pointers)
- }
- } else {
- result.exceptionOrNull()?.let {
- ErrorUtils.handleError(it)
- }
- }
- }
- }
-
- private suspend fun savePointers(
- conversationId: ID,
- pointers: List
- ) {
- pointers
- .onEach {
- db.conversationPointersDao().insert(
- conversationId = conversationId,
- messageId = it.messageId,
- status = it.messageStatus
- )
- }
- }
-
- override fun closeChatStream() {
- stream?.destroy()
- }
-
- override suspend fun hasInteracted(messageId: ID): Boolean {
- // TODO: will require conversation model update
- return false
- }
-
- override suspend fun revealIdentity(
- conversationId: ID,
- platform: Platform,
- username: String
- ): Result {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair
- ?: return Result.failure(Throwable("owner not found"))
- val chat = historyController.findChat { it.id == conversationId }
- ?: return Result.failure(Throwable("Chat not found"))
-
- val memberId = chat.selfId ?: return Result.failure(Throwable("Not member of chat"))
-
- return chatService.revealIdentity(owner, chat, memberId, platform, username)
- .map { }
- .onSuccess {
- db.conversationDao().revealIdentity(conversationId)
- }
- }
-
- override suspend fun resetUnreadCount(conversationId: ID) {
- val chat = historyController.findChat { it.id == conversationId } ?: return
- historyController.resetUnreadCount(chat.id)
- }
-
- override suspend fun advanceReadPointer(
- conversationId: ID,
- messageId: ID,
- status: MessageStatus
- ) {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: return
-
- val chat = historyController.findChat { it.id == conversationId } ?: return
-
- val memberId = chat.selfId ?: return
-
- chatService.advancePointer(owner, conversationId, memberId, messageId, status)
- .onSuccess {
- trace("advanced pointer for chat on ${messageId.description} => $status")
- }.onFailure { it.printStackTrace() }
- }
-
- override suspend fun sendMessage(conversationId: ID, message: String): Result {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair
- ?: return Result.failure(Throwable("Owner not found"))
-
- val chat = historyController.findChat { it.id == conversationId }
- ?: return Result.failure(Throwable("Chat not found"))
-
- val memberId = chat.selfId ?: return Result.failure(Throwable("Not a member of this chat"))
-
- return chatService.sendMessage(
- owner = owner,
- chat = chat,
- memberId = memberId,
- content = OutgoingMessageContent.Text(message)
- ).map {
- val messageWithContent = messageWithContentMapper.map(conversationId to it)
- CoroutineScope(Dispatchers.IO).launch {
- db.conversationMessageDao().upsertMessagesWithContent(messageWithContent)
- }
-
- messageWithContent.message.id
- }
- }
-
- override fun conversationPagingData(conversationId: ID) =
- Pager(
- config = pagingConfig,
- initialKey = null,
- ) { conversationPagingSource(conversationId) }.flow
-
- override fun observeTyping(conversationId: ID): Flow =
- typingChats.map { it.contains(conversationId) }
-
- override suspend fun onUserStartedTypingIn(conversationId: ID) {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: return
-
- val chat = historyController.findChat { it.id == conversationId }
- ?: return
-
- val memberId = chat.selfId ?: return
-
- chatService.onStartedTyping(
- owner, chat, memberId
- ).onSuccess {
- println("on typing started reported")
- }.onFailure {
- if (BuildConfig.DEBUG) {
- it.printStackTrace()
- }
- }
- }
-
- override suspend fun onUserStoppedTypingIn(conversationId: ID) {
- val owner = SessionManager.getOrganizer()?.ownerKeyPair ?: return
-
- val chat = historyController.findChat { it.id == conversationId }
- ?: return
-
- val memberId = chat.selfId ?: return
-
- chatService.onStoppedTyping(
- owner, chat, memberId
- ).onSuccess {
- println("on typing stopped reported")
- }.onFailure {
- if (BuildConfig.DEBUG) {
- it.printStackTrace()
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/network/ConversationListController.kt b/api/src/main/java/com/getcode/network/ConversationListController.kt
deleted file mode 100644
index 64ccf6bdd..000000000
--- a/api/src/main/java/com/getcode/network/ConversationListController.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.getcode.network
-
-import androidx.paging.PagingSource
-import androidx.paging.PagingState
-import com.getcode.model.chat.Chat
-import javax.inject.Inject
-
-class ConversationListController @Inject constructor(
- private val historyController: ChatHistoryController,
-) {
- val isLoading: Boolean
- get() = historyController.loadingMessages
-
- fun observeConversations() = historyController.chats
-
- suspend fun fetchChats() = historyController.fetch(true)
-}
-
-class ChatPagingSource(
- private val chats: List
-) : PagingSource() {
-
- override fun getRefreshKey(state: PagingState): Int? {
- return state.anchorPosition?.let { anchorPosition ->
- val anchorPage = state.closestPageToPosition(anchorPosition)
- anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
- }
- }
-
- override suspend fun load(params: LoadParams): LoadResult {
- val currentList = chats
- val position = params.key ?: 0
- val pageSize = params.loadSize
-
- return try {
- val items = currentList.subList(
- position.coerceAtMost(currentList.size),
- (position + pageSize).coerceAtMost(currentList.size)
- )
-
- LoadResult.Page(
- data = items,
- prevKey = if (position > 0) position - pageSize else null,
- nextKey = if (position + pageSize < currentList.size) position + pageSize else null
- )
- } catch (e: Exception) {
- LoadResult.Error(e)
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/network/api/ChatApiV2.kt b/api/src/main/java/com/getcode/network/api/ChatApiV2.kt
deleted file mode 100644
index 23045dacf..000000000
--- a/api/src/main/java/com/getcode/network/api/ChatApiV2.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-package com.getcode.network.api
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.codeinc.gen.chat.v2.ChatService.ChatMemberIdentity
-import com.codeinc.gen.chat.v2.ChatService.Content
-import com.codeinc.gen.chat.v2.ChatService.NotifyIsTypingRequest
-import com.codeinc.gen.chat.v2.ChatService.NotifyIsTypingResponse
-import com.codeinc.gen.chat.v2.ChatService.PointerType
-import com.codeinc.gen.chat.v2.ChatService.RevealIdentityRequest
-import com.codeinc.gen.chat.v2.ChatService.RevealIdentityResponse
-import com.codeinc.gen.chat.v2.ChatService.SendMessageRequest
-import com.codeinc.gen.chat.v2.ChatService.SendMessageResponse
-import com.codeinc.gen.common.v1.Model
-import com.getcode.ed25519.Ed25519.KeyPair
-import com.getcode.model.Cursor
-import com.getcode.model.ID
-import com.getcode.model.SocialUser
-import com.getcode.model.chat.ChatMember
-import com.getcode.model.chat.OutgoingMessageContent
-import com.getcode.model.chat.Platform
-import com.getcode.model.chat.StartChatRequest
-import com.getcode.model.chat.StartChatResponse
-import com.getcode.network.core.GrpcApi
-import com.getcode.network.repository.toByteString
-import com.getcode.network.repository.toSolanaAccount
-import com.getcode.solana.keys.PublicKey
-import com.getcode.utils.bytes
-import com.getcode.utils.sign
-import io.grpc.ManagedChannel
-import io.grpc.stub.StreamObserver
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOn
-import java.util.UUID
-import javax.inject.Inject
-import com.getcode.model.chat.AdvancePointerRequestV2 as AdvancePointerRequest
-import com.getcode.model.chat.AdvancePointerResponseV2 as AdvancePointerResponse
-import com.getcode.model.chat.ChatCursorV2 as ChatCursor
-import com.getcode.model.chat.ChatGrpcV2 as ChatGrpc
-import com.getcode.model.chat.ChatIdV2 as ChatId
-import com.getcode.model.chat.GetChatsRequestV2 as GetChatsRequest
-import com.getcode.model.chat.GetChatsResponseV2 as GetChatsResponse
-import com.getcode.model.chat.GetMessagesDirectionV2 as GetMessagesDirection
-import com.getcode.model.chat.GetMessagesRequestV2 as GetMessagesRequest
-import com.getcode.model.chat.GetMessagesResponseV2 as GetMessagesResponse
-import com.getcode.model.chat.ModelIntentId as IntentId
-import com.getcode.model.chat.PointerV2 as Pointer
-import com.getcode.model.chat.SetMuteStateRequestV2 as SetMuteStateRequest
-import com.getcode.model.chat.SetMuteStateResponseV2 as SetMuteStateResponse
-import com.getcode.model.chat.SetSubscriptionStateRequestV2 as SetSubscriptionStateRequest
-import com.getcode.model.chat.SetSubscriptionStateResponseV2 as SetSubscriptionStateResponse
-
-class ChatApiV2 @Inject constructor(
- managedChannel: ManagedChannel
-) : GrpcApi(managedChannel) {
- private val api = ChatGrpc.newStub(managedChannel).withWaitForReady()
-
- fun startChat(
- owner: KeyPair,
- self: SocialUser,
- with: SocialUser,
- intentId: ID
- ): Flow {
- val request = StartChatRequest.newBuilder()
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .setSelf(self.chatMemberIdentity)
- .setTwoWayChat(
- ChatService.StartTwoWayChatParameters.newBuilder()
- .setIdentity(with.chatMemberIdentity)
- .setIntentId(IntentId.newBuilder().setValue(intentId.toByteString()))
- .setOtherUser(with.tipAddress.bytes.toSolanaAccount())
- .build()
- )
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::startChat
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun fetchChats(owner: KeyPair): Flow {
- val request = GetChatsRequest.newBuilder()
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::getChats
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun fetchChatMessages(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- cursor: Cursor? = null,
- limit: Int? = null
- ): Flow {
- val builder = GetMessagesRequest.newBuilder()
- .setChatId(
- ChatId.newBuilder()
- .setValue(chatId.toByteString())
- .build()
- ).setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- )
-
- if (cursor != null) {
- builder.setCursor(
- ChatCursor.newBuilder()
- .setValue(cursor.toByteString())
- )
- }
-
- if (limit != null) {
- builder.setPageSize(limit)
- }
-
- builder.setDirection(GetMessagesDirection.DESC)
-
- val request = builder
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::getMessages
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun advancePointer(owner: KeyPair, chatId: ID, memberId: UUID, to: ID, type: PointerType): Flow {
- val request = AdvancePointerRequest.newBuilder()
- .setChatId(
- ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- .build()
- ).setPointer(
- Pointer.newBuilder()
- .setType(type)
- .setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString()))
- .setValue(
- ChatService.ChatMessageId.newBuilder()
- .setValue(to.toByteArray().toByteString())
- )
- ).setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::advancePointer
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Flow {
- val request = SetMuteStateRequest.newBuilder()
- .setChatId(
- ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- .build()
- ).setIsMuted(muted)
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::setMuteState
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun setSubscriptionState(
- owner: KeyPair,
- chatId: ID,
- subscribed: Boolean
- ): Flow {
- val request = SetSubscriptionStateRequest.newBuilder()
- .setChatId(
- ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- .build()
- ).setIsSubscribed(subscribed)
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- return api::setSubscriptionState
- .callAsCancellableFlow(request)
- .flowOn(Dispatchers.IO)
- }
-
- fun streamChatEvents(
- observer: StreamObserver
- ): StreamObserver? {
- return api.streamChatEvents(observer)
- }
-
-
- /**
- * ChatId chat_id = 1;
- *
- * ChatMemberId member_id = 2;
- *
- * // Allowed content types that can be sent by client:
- * // - TextContent
- * // - ThankYouContent
- * repeated Content content = 3;
- *
- * common.v1.SolanaAccountId owner = 4;
- *
- * common.v1.Signature signature = 5;
- */
- fun sendMessage(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- content: OutgoingMessageContent,
- observer: StreamObserver
- ) {
- val contentProto = when (content) {
- is OutgoingMessageContent.Text -> Content.newBuilder()
- .setText(ChatService.TextContent.newBuilder().setText(content.text))
- is OutgoingMessageContent.ThankYou -> Content.newBuilder()
- .setThankYou(ChatService.ThankYouContent.newBuilder()
- .setTipIntent(Model.IntentId.newBuilder()
- .setValue(content.tipIntentId.toByteString()))
- )
- }
-
- val request = SendMessageRequest.newBuilder()
- .setChatId(ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- )
- .addContent(contentProto)
- .setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- ).setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- api.sendMessage(request, observer)
- }
-
- fun revealIdentity(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- platform: Platform,
- username: String,
- observer: StreamObserver
- ) {
- val request = RevealIdentityRequest.newBuilder()
- .setChatId(ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- )
- .setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- )
- .setIdentity(ChatMemberIdentity.newBuilder()
- .setPlatformValue(platform.ordinal)
- .setUsername(username)
- )
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- api.revealIdentity(request, observer)
- }
-
- fun onStartedTyping(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- observer: StreamObserver
- ) {
- val request = NotifyIsTypingRequest.newBuilder()
- .setChatId(ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- ).setIsTyping(true)
- .setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- )
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- api.notifyIsTyping(request, observer)
- }
-
- fun onStoppedTyping(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- observer: StreamObserver
- ) {
- val request = NotifyIsTypingRequest.newBuilder()
- .setChatId(ChatId.newBuilder()
- .setValue(chatId.toByteArray().toByteString())
- ).setIsTyping(false)
- .setMemberId(ChatService.ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- )
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- .build()
-
- api.notifyIsTyping(request, observer)
- }
-}
-
-private val SocialUser.chatMemberIdentity: ChatMemberIdentity
- get() {
- val builder = ChatMemberIdentity.newBuilder()
- .setUsername(username)
- .setPlatform(
- when (Platform.named(platform)) {
- Platform.Unknown -> ChatService.Platform.UNKNOWN_PLATFORM
- Platform.Twitter -> ChatService.Platform.TWITTER
- }
- )
-
- if (imageUrl != null) {
- builder.setProfilePicUrl(imageUrl)
- }
-
- return builder.build()
- }
\ No newline at end of file
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
deleted file mode 100644
index 004551522..000000000
--- a/api/src/main/java/com/getcode/network/client/Client_Chat.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package com.getcode.network.client
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.getcode.ed25519.Ed25519.KeyPair
-import com.getcode.manager.SessionManager
-import com.getcode.model.Cursor
-import com.getcode.model.Domain
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.ConversationEntity
-import com.getcode.model.chat.NotificationCollectionEntity
-import com.getcode.model.chat.isV2
-import com.getcode.network.core.BidirectionalStreamReference
-import com.getcode.network.repository.base58
-import com.getcode.utils.TraceType
-import com.getcode.utils.trace
-import timber.log.Timber
-import java.util.UUID
-
-typealias ChatMessageStreamReference = BidirectionalStreamReference
-
-data class ChatFetchExceptions(val errors: List): Throwable()
-
-suspend fun Client.fetchChats(owner: KeyPair): Result> {
- val v2Chats = fetchV2Chats(owner)
- val v1Chats = fetchV1Chats(owner)
-
- if (v2Chats.isSuccess || v1Chats.isSuccess) {
- val chats = (v1Chats.getOrNull().orEmpty() + v2Chats.getOrNull().orEmpty())
- .sortedByDescending { it.lastMessageMillis }
- .distinctBy { it.id }
-
- return Result.success(chats)
- } else {
- val errors: List =
- listOfNotNull(v1Chats.exceptionOrNull(), v2Chats.exceptionOrNull())
- return Result.failure(ChatFetchExceptions(errors))
- }
-}
-
-suspend fun Client.fetchV1Chats(owner: KeyPair): Result> {
- val v1Chats = chatServiceV1.fetchChats(owner)
- .onSuccess {
- Timber.d("v1 chats fetched=${it.count()}")
- }.onFailure {
- trace("Failed fetching chats from V1", type = TraceType.Error)
- }
-
- return v1Chats
- .map { chats ->
- chats.sortedByDescending { it.lastMessageMillis }
- .distinctBy { it.id }
- }
-}
-
-suspend fun Client.fetchV2Chats(owner: KeyPair): Result> {
- val v2Chats = chatServiceV2.fetchChats(owner)
- .onSuccess {
- Timber.d("v2 chats fetched=${it.count()}")
- }.onFailure {
- trace("Failed fetching chats from V2", type = TraceType.Error)
- }
-
- return v2Chats
- .map { chats ->
- chats.sortedByDescending { it.lastMessageMillis }
- .distinctBy { it.id }
- }
-}
-
-
-suspend fun Client.setMuted(owner: KeyPair, chat: Chat, muted: Boolean): Result {
- return if (chat.isV2) {
- chatServiceV2.setMuteState(owner, chat.id, muted)
- } else {
- chatServiceV1.setMuteState(owner, chat.id, muted)
- }
-}
-
-suspend fun Client.setSubscriptionState(
- owner: KeyPair,
- chat: Chat,
- subscribed: Boolean
-): Result {
- return if (chat.isV2) {
- chatServiceV2.setSubscriptionState(owner, chat.id, subscribed)
- } else {
- chatServiceV1.setSubscriptionState(owner, chat.id, subscribed)
- }
-}
-
-suspend fun Client.fetchMessagesFor(
- owner: KeyPair,
- chat: Chat,
- cursor: Cursor? = null,
- limit: Int? = null
-): Result> {
- val result = if (chat.isV2) {
- chatServiceV2.fetchMessagesFor(owner, chat, cursor, limit)
- } else {
- chatServiceV1.fetchMessagesFor(owner, chat, cursor, limit)
- }
-
- return result
- .mapCatching { messages ->
- val organizer = SessionManager.getOrganizer() ?: return@mapCatching messages
- val domain = Domain.from(chat.title?.value) ?: return@mapCatching messages
-
- val relationship = organizer.relationshipFor(domain) ?: return@mapCatching messages
-
- val hasEncryptedContent = messages.firstOrNull { it.hasEncryptedContent } != null
- if (hasEncryptedContent) {
- messages.map { message ->
- message.decryptingUsing(relationship.getCluster().authority.keyPair)
- }
- } else {
- messages
- }
- }
- .onSuccess {
- Timber.d("messages fetched=${it.count()} for ${chat.id.base58}")
- Timber.d("start=${it.minOf { it.dateMillis }}, end=${it.maxOf { it.dateMillis }}")
- }.onFailure {
- Timber.e(t = it, "Failed fetching messages.")
- }
-}
-
-suspend fun Client.advancePointer(
- owner: KeyPair,
- chat: Chat,
- to: ID,
- memberId: UUID? = null,
- status: MessageStatus = MessageStatus.Read,
-): Result {
- return if (chat.isV2) {
- memberId ?: return Result.failure(Throwable("member ID was not provided"))
- chatServiceV2.advancePointer(owner, chat.id, memberId, to, status)
- } else {
- chatServiceV1.advancePointer(owner, chat.id, to, status)
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt b/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt
deleted file mode 100644
index 0c01599be..000000000
--- a/api/src/main/java/com/getcode/network/service/ChatServiceV2.kt
+++ /dev/null
@@ -1,799 +0,0 @@
-package com.getcode.network.service
-
-import com.codeinc.gen.chat.v2.ChatService
-import com.codeinc.gen.chat.v2.ChatService.ChatMemberId
-import com.codeinc.gen.chat.v2.ChatService.NotifyIsTypingResponse
-import com.codeinc.gen.chat.v2.ChatService.OpenChatEventStream
-import com.codeinc.gen.chat.v2.ChatService.PointerType
-import com.codeinc.gen.chat.v2.ChatService.RevealIdentityResponse
-import com.codeinc.gen.chat.v2.ChatService.SendMessageResponse
-import com.codeinc.gen.chat.v2.ChatService.StreamChatEventsRequest
-import com.codeinc.gen.chat.v2.ChatService.StreamChatEventsResponse
-import com.codeinc.gen.common.v1.Model.ClientPong
-import com.getcode.ed25519.Ed25519.KeyPair
-import com.getcode.mapper.ChatMessageV2Mapper
-import com.getcode.mapper.ChatMetadataV2Mapper
-import com.getcode.mapper.PointerMapper
-import com.getcode.model.Conversation
-import com.getcode.model.Cursor
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.SocialUser
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.ChatIdV2
-import com.getcode.model.chat.ChatStreamEventUpdate
-import com.getcode.model.chat.ChatType
-import com.getcode.model.chat.OutgoingMessageContent
-import com.getcode.model.chat.Platform
-import com.getcode.model.description
-import com.getcode.model.uuid
-import com.getcode.network.api.ChatApiV2
-import com.getcode.network.client.ChatMessageStreamReference
-import com.getcode.network.core.NetworkOracle
-import com.getcode.network.repository.sign
-import com.getcode.network.repository.toByteString
-import com.getcode.network.repository.toSolanaAccount
-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
-import io.grpc.Status
-import io.grpc.StatusRuntimeException
-import io.grpc.stub.StreamObserver
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.suspendCancellableCoroutine
-import timber.log.Timber
-import java.util.UUID
-import javax.inject.Inject
-import kotlin.coroutines.resume
-
-/**
- * Abstraction layer to handle [ChatApiV2] request results and map to domain models
- */
-class ChatServiceV2 @Inject constructor(
- private val api: ChatApiV2,
- private val chatMapper: ChatMetadataV2Mapper,
- private val messageMapper: ChatMessageV2Mapper,
- private val pointerMapper: PointerMapper,
- private val networkOracle: NetworkOracle,
-) {
- private fun observeChats(owner: KeyPair): Flow>> {
- return networkOracle.managedRequest(api.fetchChats(owner))
- .map { response ->
- when (response.result) {
- ChatService.GetChatsResponse.Result.OK -> {
- Result.success(response.chatsList.map(chatMapper::map))
- }
-
- ChatService.GetChatsResponse.Result.NOT_FOUND -> {
- val error = Throwable("Error: chats not found for owner")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.GetChatsResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
- }
- }
-
- @Throws(NoSuchElementException::class)
- suspend fun fetchChats(owner: KeyPair): Result> {
- return runCatching { observeChats(owner).first() }.getOrDefault(Result.success(emptyList()))
- }
-
- suspend fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Result {
- return try {
- networkOracle.managedRequest(api.setMuteState(owner, chatId, muted))
- .map { response ->
- when (response.result) {
- ChatService.SetMuteStateResponse.Result.OK -> {
- Result.success(muted)
- }
-
- ChatService.SetMuteStateResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: chat not found for $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.SetMuteStateResponse.Result.CANT_MUTE -> {
- val error = Throwable("Error: Unable to change mute state for $chatId.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.SetMuteStateResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
- }.first()
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- Result.failure(e)
- }
- }
-
- suspend fun setSubscriptionState(
- owner: KeyPair,
- chatId: ID,
- subscribed: Boolean,
- ): Result {
- return try {
- networkOracle.managedRequest(api.setSubscriptionState(owner, chatId, subscribed))
- .map { response ->
- when (response.result) {
- ChatService.SetSubscriptionStateResponse.Result.OK -> {
- Result.success(subscribed)
- }
-
- ChatService.SetSubscriptionStateResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: chat not found for $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.SetSubscriptionStateResponse.Result.CANT_UNSUBSCRIBE -> {
- val error = Throwable("Error: Unable to change mute state for $chatId.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.SetSubscriptionStateResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
- }.first()
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- Result.failure(e)
- }
- }
-
- suspend fun fetchMessagesFor(
- owner: KeyPair,
- chat: Chat,
- cursor: Cursor? = null,
- limit: Int? = null
- ): Result> {
- return try {
- val memberId = chat.members
- .filter { it.isSelf }
- .map { it.id }
- .firstOrNull()
- ?: throw IllegalStateException("Fetching messages for a chat you are not a member in")
-
- networkOracle.managedRequest(
- api.fetchChatMessages(
- owner,
- chat.id,
- memberId,
- cursor,
- limit
- )
- )
- .map { response ->
- when (response.result) {
- ChatService.GetMessagesResponse.Result.OK -> {
- Result.success(response.messagesList.map {
- messageMapper.map(chat to it)
- })
- }
-
- ChatService.GetMessagesResponse.Result.MESSAGE_NOT_FOUND -> {
- val error =
- Throwable("Error: messages not found for chat ${chat.id.description}")
- Result.failure(error)
- }
-
- ChatService.GetMessagesResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Result.failure(error)
- }
- }
- }.first()
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- Result.failure(e)
- }
- }
-
- suspend fun advancePointer(
- owner: KeyPair,
- chatId: ID,
- memberId: UUID,
- to: ID,
- status: MessageStatus,
- ): Result {
-
- val type = when (status) {
- MessageStatus.Sent -> PointerType.SENT
- MessageStatus.Delivered -> PointerType.DELIVERED
- MessageStatus.Read -> PointerType.READ
- MessageStatus.Unknown -> return Result.failure(Throwable("Can't update a pointer to Unknown"))
- }
- return try {
- networkOracle.managedRequest(api.advancePointer(owner, chatId, memberId, to, type))
- .map { response ->
- when (response.result) {
- ChatService.AdvancePointerResponse.Result.OK -> {
- Result.success(Unit)
- }
-
- ChatService.AdvancePointerResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: chat not found $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.AdvancePointerResponse.Result.MESSAGE_NOT_FOUND -> {
- val error = Throwable("Error: message not found $to")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.AdvancePointerResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
- }.first()
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- Result.failure(e)
- }
- }
-
- suspend fun startChat(
- owner: KeyPair,
- self: SocialUser,
- with: SocialUser,
- intentId: ID,
- type: ChatType
- ): Result {
- trace("Creating $type chat for ${intentId.description}")
- return when (type) {
- ChatType.Unknown -> throw IllegalArgumentException("Unknown chat type provided")
- ChatType.Notification -> throw IllegalArgumentException("Unable to create notification chats from client")
- ChatType.TwoWay -> {
- try {
- networkOracle.managedRequest(api.startChat(owner, self, with, intentId))
- .map { response ->
- when (response.result) {
- ChatService.StartChatResponse.Result.OK -> {
- trace("Chat created for ${intentId.description}")
- Result.success(chatMapper.map(response.chat))
- }
-
- ChatService.StartChatResponse.Result.DENIED -> {
- val error = Throwable("Error: Denied")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.StartChatResponse.Result.INVALID_PARAMETER -> {
- val error = Throwable("Error: Invalid parameter")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- ChatService.StartChatResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
- }.first()
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- Result.failure(e)
- }
- }
- }
- }
-
- fun openChatStream(
- scope: CoroutineScope,
- conversation: Conversation,
- memberId: UUID,
- owner: KeyPair,
- chatLookup: (Conversation) -> Chat,
- onEvent: (Result) -> Unit
- ): ChatMessageStreamReference {
- trace("Chat ${conversation.id.description} Opening stream.")
- val streamReference = ChatMessageStreamReference(scope)
- streamReference.retain()
- streamReference.timeoutHandler = {
- trace("Chat ${conversation.id.description} Stream timed out")
- openChatStream(
- conversation = conversation,
- memberId = memberId,
- owner = owner,
- reference = streamReference,
- chatLookup = chatLookup,
- onEvent = onEvent
- )
- }
-
- openChatStream(conversation, memberId, owner, streamReference, chatLookup, onEvent)
-
- return streamReference
- }
-
- private fun openChatStream(
- conversation: Conversation,
- memberId: UUID,
- owner: KeyPair,
- reference: ChatMessageStreamReference,
- chatLookup: (Conversation) -> Chat,
- onEvent: (Result) -> Unit
- ) {
- try {
- reference.cancel()
- reference.stream =
- api.streamChatEvents(object : StreamObserver {
- override fun onNext(value: StreamChatEventsResponse?) {
- val result = value?.typeCase
- if (result == null) {
- trace(
- message = "Chat ${conversation.id.description} Server sent empty message. This is unexpected.",
- type = TraceType.Error
- )
- return
- }
-
- when (result) {
- StreamChatEventsResponse.TypeCase.EVENTS -> {
- val pointerStatuses = value.events.eventsList
- .map { it.pointer }
- .mapNotNull { pointerMapper.map(it) }
-
- val messages = value.events.eventsList
- .map { it.message }
- .map { messageMapper.map(chatLookup(conversation) to it) }
-
- val isTyping = value.events.eventsList
- .map { it.isTyping }
- .find { it.isTyping && it.memberId.value.toList().uuid != memberId } != null
-
- trace("Chat ${conversation.id.description} received ${messages.count()} messages and ${pointerStatuses.count()} status updates.")
- val update = ChatStreamEventUpdate(messages, pointerStatuses, isTyping)
- onEvent(Result.success(update))
- }
-
- StreamChatEventsResponse.TypeCase.PING -> {
- val stream = reference.stream ?: return
- val request = StreamChatEventsRequest.newBuilder()
- .setPong(
- ClientPong.newBuilder()
- .setTimestamp(
- Timestamp.newBuilder()
- .setSeconds(System.currentTimeMillis() / 1_000)
- )
- ).build()
-
- reference.receivedPing(updatedTimeout = value.ping.pingDelay.seconds * 1_000L)
- stream.onNext(request)
- trace("Pong Chat ${conversation.id.description} Server timestamp: ${value.ping.timestamp}")
- }
-
- StreamChatEventsResponse.TypeCase.TYPE_NOT_SET -> Unit
- StreamChatEventsResponse.TypeCase.ERROR -> {
- trace(
- type = TraceType.Error,
- message = "Chat ${conversation.id.description} hit a snag. ${value.error.code}"
- )
- }
- }
- }
-
- override fun onError(t: Throwable?) {
- val statusException = t as? StatusRuntimeException
- if (statusException?.status?.code == Status.Code.UNAVAILABLE) {
- trace("Chat ${conversation.id.description} Reconnecting keepalive stream...")
- openChatStream(
- conversation,
- memberId,
- owner,
- reference,
- chatLookup,
- onEvent
- )
- } else {
- t?.printStackTrace()
- }
- }
-
- override fun onCompleted() {
-
- }
- })
-
- val request = StreamChatEventsRequest.newBuilder()
- .setOpenStream(OpenChatEventStream.newBuilder()
- .setChatId(
- ChatIdV2.newBuilder()
- .setValue(conversation.id.toByteString())
- .build()
- )
- .setMemberId(
- ChatMemberId.newBuilder()
- .setValue(memberId.bytes.toByteString())
- )
- .setOwner(owner.publicKeyBytes.toSolanaAccount())
- .apply { setSignature(sign(owner)) }
- ).build()
-
- reference.stream?.onNext(request)
- trace("Chat ${conversation.id.description} Initiating a connection...")
- } catch (e: Exception) {
- if (e is IllegalStateException && e.message == "call already half-closed") {
- // ignore
- } else {
- ErrorUtils.handleError(e)
- }
- }
- }
-
- suspend fun sendMessage(
- owner: KeyPair,
- chat: Chat,
- memberId: UUID,
- content: OutgoingMessageContent,
- ): Result = suspendCancellableCoroutine { cont ->
- val chatId = chat.id
- try {
- api.sendMessage(
- owner = owner,
- chatId = chatId,
- memberId = memberId,
- content = content,
- observer = object : StreamObserver {
- override fun onNext(value: SendMessageResponse?) {
- val requestResult = value?.result
- if (requestResult == null) {
- trace(
- message = "Chat SendMessage Server returned empty message. This is unexpected.",
- type = TraceType.Error
- )
- return
- }
-
- val result = when (requestResult) {
- SendMessageResponse.Result.OK -> {
- trace("Chat message sent =: ${value.message.messageId.value.toList().description}")
- val message = messageMapper.map(chat to value.message)
- Result.success(message)
- }
-
- SendMessageResponse.Result.DENIED -> {
- val error = Throwable("Error: Send Message: Denied")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- SendMessageResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: Send Message: chat not found $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- SendMessageResponse.Result.INVALID_CHAT_TYPE -> {
- val error = Throwable("Error: Send Message: invalid chat type")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- SendMessageResponse.Result.INVALID_CONTENT_TYPE -> {
- val error = Throwable("Error: Send Message: invalid content type")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- SendMessageResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Send Message: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
-
- cont.resume(result)
- }
-
- override fun onError(t: Throwable?) {
- val error = t ?: Throwable("Error: Hit a snag")
- ErrorUtils.handleError(error)
- cont.resume(Result.failure(error))
- }
-
- override fun onCompleted() {
-
- }
-
- }
- )
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- cont.resume(Result.failure(e))
- }
- }
-
- suspend fun revealIdentity(
- owner: KeyPair,
- chat: Chat,
- memberId: UUID,
- platform: Platform,
- username: String,
- ): Result = suspendCancellableCoroutine { cont ->
- val chatId = chat.id
- try {
- api.revealIdentity(
- owner,
- chatId,
- memberId,
- platform,
- username,
- observer = object : StreamObserver {
- override fun onNext(value: RevealIdentityResponse?) {
- val requestResult = value?.result
- if (requestResult == null) {
- trace(
- message = "Chat SendMessage Server returned empty message. This is unexpected.",
- type = TraceType.Error
- )
- return
- }
-
- val result = when (requestResult) {
- RevealIdentityResponse.Result.OK -> {
- trace("Chat message sent =: ${value.message.messageId.value.toList().description}")
- val message = messageMapper.map(chat to value.message)
- Result.success(message)
- }
-
- RevealIdentityResponse.Result.DENIED -> {
- val error = Throwable("Error: Send Message: Denied")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- RevealIdentityResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: Send Message: chat not found $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- RevealIdentityResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Send Message: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
-
- cont.resume(result)
- }
-
- override fun onError(t: Throwable?) {
- val error = t ?: Throwable("Error: Hit a snag")
- ErrorUtils.handleError(error)
- cont.resume(Result.failure(error))
- }
-
- override fun onCompleted() {
-
- }
-
- }
- )
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- cont.resume(Result.failure(e))
- }
- }
-
- suspend fun onStartedTyping(
- owner: KeyPair,
- chat: Chat,
- memberId: UUID,
- ): Result = suspendCancellableCoroutine { cont ->
- val chatId = chat.id
- try {
- api.onStartedTyping(
- owner,
- chatId,
- memberId,
- observer = object : StreamObserver {
- override fun onNext(value: NotifyIsTypingResponse?) {
- val requestResult = value?.result
- if (requestResult == null) {
- trace(
- message = "Chat NotifyIsTyping Server returned empty message. This is unexpected.",
- type = TraceType.Error
- )
- return
- }
-
- val result = when (requestResult) {
- NotifyIsTypingResponse.Result.OK -> {
- Result.success(Unit)
- }
-
- NotifyIsTypingResponse.Result.DENIED -> {
- val error = Throwable("Error: Send Message: Denied")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- NotifyIsTypingResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: Send Message: chat not found $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- NotifyIsTypingResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: Send Message: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
-
- cont.resume(result)
- }
-
- override fun onError(t: Throwable?) {
- val error = t ?: Throwable("Error: Hit a snag")
- ErrorUtils.handleError(error)
- cont.resume(Result.failure(error))
- }
-
- override fun onCompleted() {
-
- }
-
- }
- )
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- cont.resume(Result.failure(e))
- }
- }
-
- suspend fun onStoppedTyping(
- owner: KeyPair,
- chat: Chat,
- memberId: UUID,
- ): Result = suspendCancellableCoroutine { cont ->
- val chatId = chat.id
- try {
- api.onStoppedTyping(
- owner,
- chatId,
- memberId,
- observer = object : StreamObserver {
- override fun onNext(value: NotifyIsTypingResponse?) {
- val requestResult = value?.result
- if (requestResult == null) {
- trace(
- message = "Chat NotifyIsTyping Server returned empty message. This is unexpected.",
- type = TraceType.Error
- )
- return
- }
-
- val result = when (requestResult) {
- NotifyIsTypingResponse.Result.OK -> {
- Result.success(Unit)
- }
-
- NotifyIsTypingResponse.Result.DENIED -> {
- val error = Throwable("Error: NotifyIsTyping: Denied")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- NotifyIsTypingResponse.Result.CHAT_NOT_FOUND -> {
- val error = Throwable("Error: NotifyIsTyping: chat not found $chatId")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- NotifyIsTypingResponse.Result.UNRECOGNIZED -> {
- val error = Throwable("Error: NotifyIsTyping: Unrecognized request.")
- Timber.e(t = error)
- Result.failure(error)
- }
-
- else -> {
- val error = Throwable("Error: Unknown")
- Timber.e(t = error)
- Result.failure(error)
- }
- }
-
- cont.resume(result)
- }
-
- override fun onError(t: Throwable?) {
- val error = t ?: Throwable("Error: Hit a snag")
- ErrorUtils.handleError(error)
- cont.resume(Result.failure(error))
- }
-
- override fun onCompleted() {
-
- }
-
- }
- )
- } catch (e: Exception) {
- ErrorUtils.handleError(e)
- cont.resume(Result.failure(e))
- }
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt b/api/src/main/java/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt
deleted file mode 100644
index 6762842d2..000000000
--- a/api/src/main/java/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.getcode.solana.instructions.programs
-
-import com.getcode.solana.keys.PublicKey
-import com.getcode.vendor.Base58
-
-class AssociatedTokenProgram {
- companion object {
- val address = PublicKey(Base58.decode("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").toList())
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/solana/instructions/programs/TokenProgram.kt b/api/src/main/java/com/getcode/solana/instructions/programs/TokenProgram.kt
deleted file mode 100644
index fee746c39..000000000
--- a/api/src/main/java/com/getcode/solana/instructions/programs/TokenProgram.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.getcode.solana.instructions.programs
-
-import com.getcode.solana.keys.PublicKey
-import com.getcode.vendor.Base58
-
-class TokenProgram {
- companion object {
- val address = PublicKey(Base58.decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").toList())
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/solana/keys/ProgramDerivedAccount.kt b/api/src/main/java/com/getcode/solana/keys/ProgramDerivedAccount.kt
deleted file mode 100644
index 7606e160f..000000000
--- a/api/src/main/java/com/getcode/solana/keys/ProgramDerivedAccount.kt
+++ /dev/null
@@ -1,174 +0,0 @@
-package com.getcode.solana.keys
-
-import com.getcode.crypt.Sha256Hash
-import com.getcode.model.Kin
-import com.getcode.solana.organizer.AccountCluster
-import org.kin.sdk.base.models.toUTF8Bytes
-
-data class ProgramDerivedAccount(val publicKey: PublicKey, val bump: Int)
-
-class TimelockDerivedAccounts(
- val owner: PublicKey,
- val state: ProgramDerivedAccount,
- val vault: ProgramDerivedAccount
-) {
- companion object {
- const val lockoutInDays: Long = 21
- const val dataVersion: Long = 3
-
- fun newInstance(owner: PublicKey, legacy: Boolean = false): TimelockDerivedAccounts {
- val state: ProgramDerivedAccount
- val vault: ProgramDerivedAccount
-
- if (legacy) {
- state =
- PublicKey.deriveLegacyTimelockStateAccount(owner = owner, lockout = 1_814_400)
- vault = PublicKey.deriveLegacyTimelockVaultAccount(stateAccount = state.publicKey)
- } else {
- state = PublicKey.deriveTimelockStateAccount(owner = owner, lockout = lockoutInDays)
- vault = PublicKey.deriveTimelockVaultAccount(
- stateAccount = state.publicKey,
- version = dataVersion
- )
- }
-
- return TimelockDerivedAccounts(
- owner = owner,
- state = state,
- vault = vault
- )
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as TimelockDerivedAccounts
-
- if (owner != other.owner) return false
- if (state != other.state) return false
- if (vault != other.vault) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = owner.hashCode()
- result = 31 * result + state.hashCode()
- result = 31 * result + vault.hashCode()
- return result
- }
-}
-
-class SplitterCommitmentAccounts(
- val treasury: PublicKey,
- val destination: PublicKey,
- val recentRoot: Hash,
- val transcript: Hash,
-
- val state: ProgramDerivedAccount,
- val vault: ProgramDerivedAccount,
-) {
- companion object {
- fun newInstance(
- treasury: PublicKey,
- destination: PublicKey,
- recentRoot: Hash,
- transcript: Hash,
- amount: Kin
- ): SplitterCommitmentAccounts {
- val state = PublicKey.deriveCommitmentStateAccount(
- treasury = treasury,
- recentRoot = recentRoot,
- transcript = transcript,
- destination = destination,
- amount = amount
- )
-
- val vault = PublicKey.deriveCommitmentVaultAccount(
- treasury = treasury,
- commitmentState = state.publicKey
- )
-
- return SplitterCommitmentAccounts(
- treasury = treasury,
- destination = destination,
- recentRoot = recentRoot,
- transcript = transcript,
- state = state,
- vault = vault,
- )
- }
-
- fun newInstance(
- source: AccountCluster,
- destination: PublicKey,
- amount: Kin,
- treasury: PublicKey,
- recentRoot: Hash,
- intentId: PublicKey,
- actionId: Int
- ): SplitterCommitmentAccounts {
- val transcript = SplitterTranscript(
- intentId = intentId,
- actionId = actionId,
- amount = amount,
- source = source.vaultPublicKey,
- destination = destination
- )
-
- return newInstance(
- treasury = treasury,
- destination = destination,
- recentRoot = recentRoot,
- transcript = transcript.transcriptHash,
- amount = amount
- )
- }
- }
-}
-
-data class SplitterTranscript(
- val intentId: PublicKey,
- val actionId: Int, val
- amount: Kin,
- val source: PublicKey,
- val destination: PublicKey
-) {
- val description =
- "receipt[${intentId.base58()}, $actionId]: " +
- "transfer ${amount.quarks} quarks " +
- "from ${source.base58()} to ${destination.base58()}"
-
- val transcriptHash: Hash = Hash(Sha256Hash.hash(description.toUTF8Bytes()).toList())
-}
-
-data class AssociatedTokenAccount(
- val owner: PublicKey,
- val ata: ProgramDerivedAccount,
-) {
- companion object {
- fun newInstance(owner: PublicKey, mint: Mint): AssociatedTokenAccount {
- return AssociatedTokenAccount(
- owner = owner,
- ata = PublicKey.deriveAssociatedAccount(owner = owner, mint = mint)
- )
- }
- }
-}
-
-data class PreSwapStateAccount(
- val owner: PublicKey,
- val state: ProgramDerivedAccount,
-) {
- companion object {
- fun newInstance(owner: PublicKey, source: PublicKey, destination: PublicKey, nonce: PublicKey): PreSwapStateAccount {
- return PreSwapStateAccount(
- owner = owner,
- state = PublicKey.derivePreSwapState(source, destination, nonce)
- )
- }
- }
-}
-
diff --git a/api/src/main/java/com/getcode/solana/keys/PublicKey.kt b/api/src/main/java/com/getcode/solana/keys/PublicKey.kt
deleted file mode 100644
index 54f7c35fc..000000000
--- a/api/src/main/java/com/getcode/solana/keys/PublicKey.kt
+++ /dev/null
@@ -1,236 +0,0 @@
-package com.getcode.solana.keys
-
-import com.getcode.crypt.Sha256Hash
-import com.getcode.ed25519.Ed25519
-import com.getcode.model.Kin
-import com.getcode.network.repository.toPublicKey
-import com.getcode.solana.instructions.programs.*
-import com.getcode.utils.serializer.PublicKeyAsStringSerializer
-import com.getcode.vendor.Base58
-import com.google.protobuf.ByteString
-import kotlinx.serialization.Serializable
-import org.kin.sdk.base.tools.longToByteArray
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-
-@Serializable(with = PublicKeyAsStringSerializer::class)
-class PublicKey(bytes: List) : Key32(bytes) {
-
- constructor(base58: String): this(Base58.decode(base58).toList())
-
- companion object {
-
- val kin: Mint
- get() = Mint(Base58.decode("kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6").toList())
-
- val usdc: Mint
- get() = Mint(org.kin.sdk.base.tools.Base58.decode("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").toList())
-
- fun generate(): PublicKey = Ed25519.createSeed32().toPublicKey()
-
- fun fromBase58(base58: String): PublicKey {
- return PublicKey(Base58.decode(base58).toList())
- }
-
- fun fromByteString(byteString: ByteString): PublicKey {
- return PublicKey(byteString.toByteArray().toList())
- }
-
- fun deriveAssociatedAccount(owner: PublicKey, mint: PublicKey): ProgramDerivedAccount {
- return findProgramAddress(
- seeds = listOf(owner.bytes.toByteArray(), TokenProgram.address.bytes.toByteArray(), mint.bytes.toByteArray()),
- programId = AssociatedTokenProgram.address,
- )
- }
-
- fun deriveTimelockStateAccount(
- owner: PublicKey,
- lockout: Long
- ): ProgramDerivedAccount {
- val seeds: List = listOf(
- "timelock_state".toByteArray(Charsets.UTF_8),
- Mint.kin.bytes.toByteArray(),
- timeAuthority.bytes.toByteArray(),
- owner.bytes.toByteArray(),
- byteArrayOf(lockout.toByte())
- )
-
- return findProgramAddress(
- seeds = seeds,
- programId = TimelockProgram.address,
- )
- }
-
- fun deriveTimelockVaultAccount(
- stateAccount: PublicKey,
- version: Long
- ): ProgramDerivedAccount {
- val seeds: List = listOf(
- "timelock_vault".toByteArray(Charsets.UTF_8),
- stateAccount.bytes.toByteArray(),
- byteArrayOf(version.toByte())
- )
-
- return findProgramAddress(
- seeds = seeds,
- programId = TimelockProgram.address,
- )
- }
-
- fun deriveLegacyTimelockStateAccount(
- owner: PublicKey,
- lockout: Long
- ): ProgramDerivedAccount {
- val nonce = SystemProgram.address
- val version = byteArrayOf(1)
- val pdaPadding = SystemProgram.address
-
- val seeds: List = listOf(
- "timelock_state".toByteArray(Charsets.UTF_8),
- version,
- Mint.kin.bytes.toByteArray(),
- subsidizer.bytes.toByteArray(),
- nonce.bytes.toByteArray(),
- owner.bytes.toByteArray(),
- lockout.longToByteArray(),
- pdaPadding.bytes.toByteArray()
- )
-
- return findProgramAddress(
- seeds = seeds,
- programId = TimelockProgram.legacyAddress,
- )
- }
-
- fun deriveLegacyTimelockVaultAccount(
- stateAccount: PublicKey
- ): ProgramDerivedAccount {
- val seeds: List = listOf(
- "timelock_vault".toByteArray(Charsets.UTF_8),
- stateAccount.bytes.toByteArray(),
- byteArrayOf(0)
- )
-
- return findProgramAddress(
- seeds = seeds,
- programId = TimelockProgram.legacyAddress,
- )
- }
-
- /// FindProgramAddress mirrors the implementation of the Solana SDK's FindProgramAddress. Its primary
- /// use case (for Kin and Agora) is for deriving associated accounts.
- ///
- /// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L234
- ///
- fun findProgramAddress(
- seeds: List,
- programId: PublicKey
- ): ProgramDerivedAccount {
- for (i in 0..255) {
- val bumpValue = 255 - i
- try {
- val publicKey = deriveProgramAddress(programId, listOf(*seeds.toTypedArray(), byteArrayOf(bumpValue.toByte())))
- return ProgramDerivedAccount(publicKey, bumpValue)
- } catch (e: RuntimeException) {
- //no-op
- }
- }
-
- throw Exception("Unable to find a viable program address nonce")
- }
-
- /// CreateProgramAddress mirrors the implementation of the Solana SDK's CreateProgramAddress.
- ///
- /// ProgramAddresses are public keys that _do not_ lie on the ed25519 curve to ensure that
- /// there is no associated private key. In the event that the program and seed parameters
- /// result in a valid public key, ErrInvalidPublicKey is returned.
- ///
- /// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L158
- ///
- fun deriveProgramAddress(programId: PublicKey, seeds: List): PublicKey {
- fun getMaxSeeds() = 16
-
- val buffer = ByteArrayOutputStream()
- require(seeds.size < getMaxSeeds()) { "Max seed size exceeded" }
-
- for (seed in seeds) {
- try {
- buffer.write(seed)
- } catch (e: IOException) {
- throw RuntimeException(e)
- }
- }
- try {
- buffer.write(programId.bytes.toByteArray())
- buffer.write("ProgramDerivedAddress".toByteArray())
- } catch (e: IOException) {
- throw RuntimeException(e)
- }
- val hash = Sha256Hash.hash(buffer.toByteArray())
-
- val publicKey = PublicKey(hash.toList())
-
- // Following the Solana SDK, we want to _reject_ the generated public key
- // if it's a valid compressed EdwardsPoint (on the curve).
- //
- if (Ed25519.onCurve(publicKey.bytes.toByteArray())) {
- throw RuntimeException("Invalid seeds, address must fall off the curve")
- }
-
- return PublicKey(hash.toList())
- }
-
- fun deriveCommitmentStateAccount(treasury: PublicKey, recentRoot: Hash, transcript: Hash, destination: PublicKey, amount: Kin): ProgramDerivedAccount {
- return findProgramAddress(
- programId = splitter,
- seeds = listOf(
- "commitment_state".toByteArray(Charsets.UTF_8),
- treasury.bytes.toByteArray(),
- recentRoot.bytes.toByteArray(),
- transcript.bytes.toByteArray(),
- destination.bytes.toByteArray(),
- amount.quarks.longToByteArray()
- )
- )
- }
-
- fun deriveCommitmentVaultAccount(treasury: PublicKey, commitmentState: PublicKey): ProgramDerivedAccount {
- return findProgramAddress(
- programId = splitter,
- seeds = listOf(
- "commitment_vault".toByteArray(Charsets.UTF_8),
- treasury.bytes.toByteArray(),
- commitmentState.bytes.toByteArray()
- )
- )
- }
-
- fun derivePreSwapState(
- source: PublicKey, destination: PublicKey, nonce: PublicKey
- ): ProgramDerivedAccount {
- return findProgramAddress(
- programId = SwapValidatorProgram.address,
- seeds = listOf(
- "pre_swap_state".toByteArray(Charsets.UTF_8),
- source.bytes.toByteArray(),
- destination.bytes.toByteArray(),
- nonce.bytes.toByteArray(),
- )
- )
- }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
-
- other as PublicKey
- return size == other.size && bytes == other.bytes
- }
-
- override fun hashCode(): Int {
- var result = super.hashCode()
- result = 31 * result + size
- return result
- }
-
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/utils/Timber.kt b/api/src/main/java/com/getcode/utils/Timber.kt
deleted file mode 100644
index 20c188bc5..000000000
--- a/api/src/main/java/com/getcode/utils/Timber.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.getcode.utils
-
-import com.getcode.api.BuildConfig
-import timber.log.Timber
-import kotlin.math.roundToLong
-
-fun timberTimer(message: String, block: () -> T): T {
- return if (BuildConfig.DEBUG) {
- // Prefer nanoTime over currentTimeMillis for measuring elapsed time because it does not try to
- // align with wall-time.
- val start = System.nanoTime()
- val result = block()
- Timber.d(
- "$message took ${
- (System.nanoTime() - start).toDouble().div(1_000_000).roundToLong()
- }ms",
- )
- result
- } else {
- block()
- }
-}
-
-suspend fun timberTimerSuspend(message: String, block: suspend () -> T): T {
- return if (BuildConfig.DEBUG) {
- // Prefer nanoTime over currentTimeMillis for measuring elapsed time because it does not try to
- // align with wall-time.
- val start = System.nanoTime()
- val result = block()
- Timber.d(
- "$message took ${
- (System.nanoTime() - start).toDouble().div(1_000_000).roundToLong()
- }ms",
- )
- result
- } else {
- block()
- }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/getcode/utils/serializer/ConversationMessageContentTypeSerializer.kt b/api/src/main/java/com/getcode/utils/serializer/ConversationMessageContentTypeSerializer.kt
deleted file mode 100644
index d9b893d06..000000000
--- a/api/src/main/java/com/getcode/utils/serializer/ConversationMessageContentTypeSerializer.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.getcode.utils.serializer
-
-import com.getcode.model.chat.MessageContent
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.json.Json
-
-object MessageContentSerializer : KSerializer {
- override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CMC", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): MessageContent {
- val string = decoder.decodeString()
- return Json.decodeFromString(string)
- }
-
- override fun serialize(encoder: Encoder, value: MessageContent) {
- val payload = Json.encodeToString(MessageContent.serializer(), value)
- encoder.encodeString(payload)
- }
-
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c1b817aa5..af46b23dd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -20,21 +20,19 @@ plugins {
val contributorsSigningConfig = ContributorsSignatory(rootProject)
android {
- namespace = Android.namespace
+ // static namespace
+ namespace = Android.codeNamespace
compileSdk = Android.compileSdkVersion
defaultConfig {
- applicationId = Android.namespace
versionCode = versioning.getVersionCode()
- versionName = Packaging.versionName
-
+ versionName = Packaging.Code.versionName
+ applicationId = Android.codeNamespace
minSdk = Android.minSdkVersion
targetSdk = Android.targetSdkVersion
buildToolsVersion = Android.buildToolsVersion
testInstrumentationRunner = Android.testInstrumentationRunner
- resValue("string", "applicationId", Android.namespace)
-
buildConfigField("String", "MIXPANEL_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "MIXPANEL_API_KEY")}\"")
buildConfigField("String", "KADO_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "KADO_API_KEY")}\"")
buildConfigField("Boolean", "NOTIFY_ERRORS", "false")
@@ -60,12 +58,14 @@ android {
buildTypes {
getByName("release") {
+ resValue("string", "applicationId", Android.codeNamespace)
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
getByName("debug") {
applicationIdSuffix = ".dev"
+ resValue("string", "applicationId", "${Android.codeNamespace}.dev")
signingConfig = signingConfigs.getByName("contributors")
val debugMinifyEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_MINIFY", "false").toBooleanLenient() ?: false
@@ -100,8 +100,6 @@ android {
kotlinOptions {
jvmTarget = Versions.java
freeCompilerArgs += listOf(
- "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
- "-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlin.RequiresOptIn"
)
}
@@ -115,12 +113,24 @@ android {
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:components"))
- implementation(project(":common:resources"))
- implementation(project(":common:theme"))
+ // libs (not included with services)
+ implementation(project(":libs:locale"))
+ implementation(project(":libs:vibrator"))
+ implementation(project(":libs:messaging"))
+ implementation(project(":libs:permissions"))
+ implementation(project(":libs:quickresponse"))
+ implementation(project(":libs:requests"))
+
+ // code services
+ implementation(project(":services:code"))
+
+ // ui components
+ implementation(project(":ui:components"))
+ implementation(project(":ui:navigation"))
+ implementation(project(":ui:resources"))
+ implementation(project(":ui:theme"))
+
+ // tipkit
implementation(project(":vendor:tipkit:tipkit-m2"))
coreLibraryDesugaring(Libs.android_desugaring)
@@ -128,7 +138,6 @@ dependencies {
//standard libraries
implementation(Libs.kotlinx_collections_immutable)
implementation(Libs.kotlinx_serialization_json)
- implementation(Libs.kotlinx_datetime)
implementation(Libs.androidx_core)
implementation(Libs.androidx_constraint_layout)
implementation(Libs.androidx_lifecycle_runtime)
@@ -138,7 +147,7 @@ dependencies {
//hilt dependency injection
implementation(Libs.hilt)
- implementation("androidx.webkit:webkit:1.11.0")
+ implementation("androidx.webkit:webkit:1.12.1")
kapt(Libs.hilt_android_compiler)
kapt(Libs.hilt_compiler)
androidTestImplementation(Libs.hilt)
@@ -152,8 +161,6 @@ dependencies {
//Jetpack compose
implementation(platform(Libs.compose_bom))
implementation(Libs.compose_ui)
- debugImplementation(Libs.compose_ui_tools)
- implementation(Libs.compose_ui_tools_preview)
implementation(Libs.compose_accompanist)
implementation(Libs.compose_foundation)
implementation(Libs.compose_material)
@@ -163,10 +170,6 @@ dependencies {
implementation(Libs.compose_livedata)
implementation(Libs.compose_navigation)
implementation(Libs.compose_paging)
- implementation(Libs.compose_voyager_navigation)
- implementation(Libs.compose_voyager_navigation_transitions)
- implementation(Libs.compose_voyager_navigation_bottomsheet)
- implementation(Libs.compose_voyager_navigation_hilt)
implementation(Libs.compose_webview)
implementation(Libs.androidx_biometrics)
@@ -185,9 +188,6 @@ dependencies {
implementation(Libs.androidx_browser)
implementation(Libs.androidx_constraint_layout_compose)
- implementation(Libs.rxjava)
- implementation(Libs.rxandroid)
-
implementation(Libs.slf4j)
implementation(Libs.grpc_android)
@@ -199,15 +199,12 @@ dependencies {
implementation(Libs.hilt_nav_compose)
implementation(Libs.lib_phone_number_port)
implementation(Libs.mp_android_chart)
- implementation(Libs.zxing)
implementation(Libs.mixpanel)
implementation(Libs.retrofit)
implementation(Libs.retrofit_converter)
implementation(Libs.okhttp_logging_interceptor)
- implementation(Libs.cloudy)
-
androidTestImplementation(Libs.androidx_test_runner)
androidTestImplementation(Libs.androidx_junit)
androidTestImplementation(Libs.junit)
@@ -233,4 +230,4 @@ dependencies {
implementation(Libs.bugsnag)
implementation(Libs.haze)
-}
+}
\ No newline at end of file
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..16c292d86
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/debug/res/drawable/ic_launcher_background.xml b/app/src/debug/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index a40ec6210..000000000
--- a/app/src/debug/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..01e1320a6
--- /dev/null
+++ b/app/src/debug/res/values/ic_launcher_background.xml
@@ -0,0 +1,5 @@
+
+
+ #9C0222
+
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
index 055e62a1f..1fa80e5c0 100644
--- a/app/src/debug/res/values/strings.xml
+++ b/app/src/debug/res/values/strings.xml
@@ -1,3 +1,4 @@
Code Dev
+ com.getcode.dev.accountprovider
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 76566fe3f..52f454aee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,13 +11,10 @@
-
-
-
diff --git a/app/src/main/java/com/getcode/AccountProvider.kt b/app/src/main/java/com/getcode/AccountProvider.kt
new file mode 100644
index 000000000..580aca624
--- /dev/null
+++ b/app/src/main/java/com/getcode/AccountProvider.kt
@@ -0,0 +1,57 @@
+package com.getcode
+
+import android.accounts.AccountManager
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import com.getcode.util.AccountUtils
+import kotlinx.coroutines.runBlocking
+
+class AccountProvider : ContentProvider() {
+
+ override fun onCreate(): Boolean {
+
+ return true
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor? {
+ val context = context ?: return null
+ val token = runBlocking { AccountUtils.getToken(context) }
+ val cursor = MatrixCursor(arrayOf(AccountManager.KEY_AUTHTOKEN))
+ cursor.addRow(arrayOf(token))
+ return cursor
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ // Handle insertion of new data if needed
+ return null
+ }
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
+ // Handle deletion of data if needed
+ return 0
+ }
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?
+ ): Int {
+ // Handle updating of data if needed
+ return 0
+ }
+
+ override fun getType(uri: Uri): String {
+ // Return the MIME type of data based on the URI pattern
+ return "vnd.android.cursor.dir/vnd.getcode.account"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/CodeApp.kt b/app/src/main/java/com/getcode/CodeApp.kt
index d6b1cc339..0c98629a8 100644
--- a/app/src/main/java/com/getcode/CodeApp.kt
+++ b/app/src/main/java/com/getcode/CodeApp.kt
@@ -3,7 +3,6 @@ package com.getcode
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -17,13 +16,11 @@ 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.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
-import cafe.adriel.voyager.core.screen.Screen
-import cafe.adriel.voyager.core.screen.ScreenKey
-import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.Navigator
@@ -35,18 +32,19 @@ import com.getcode.navigation.core.CombinedNavigator
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.LoginScreen
import com.getcode.navigation.transitions.SheetSlideTransition
-import com.getcode.theme.CodeTheme
import com.getcode.theme.LocalCodeColors
import com.getcode.ui.components.AuthCheck
import com.getcode.ui.components.bars.BottomBarContainer
-import com.getcode.ui.components.CodeScaffold
+import com.getcode.ui.theme.CodeScaffold
import com.getcode.ui.components.ModalContainer
import com.getcode.ui.components.OnLifecycleEvent
-import com.getcode.ui.components.TitleBar
+import com.getcode.ui.components.AppBarWithTitle
import com.getcode.ui.components.bars.TopBarContainer
import com.getcode.ui.modals.ConfirmationModals
import com.getcode.ui.utils.getActivity
-import com.getcode.ui.utils.getActivityScopedViewModel
+import com.getcode.navigation.extensions.getActivityScopedViewModel
+import com.getcode.ui.LocalTopBarPadding
+import com.getcode.ui.theme.CodeTheme
import com.getcode.ui.utils.measured
import com.getcode.ui.utils.rememberBiometricsState
import com.getcode.util.BiometricsError
@@ -56,7 +54,7 @@ import dev.bmcreations.tipkit.engines.TipsEngine
@Composable
fun CodeApp(tipsEngine: TipsEngine) {
- val tlvm = MainRoot.getActivityScopedViewModel()
+ val tlvm = getActivityScopedViewModel()
val state by tlvm.state.collectAsState()
val activity = LocalContext.current.getActivity()
val biometricsState = rememberBiometricsState(
@@ -100,9 +98,10 @@ fun CodeApp(tipsEngine: TipsEngine) {
val (isVisibleTopBar, isVisibleBackButton) = appState.isVisibleTopBar
if (isVisibleTopBar && appState.currentTitle.isNotBlank()) {
- TitleBar(
+ AppBarWithTitle(
modifier = Modifier.measured { topBarHeight = it.height },
title = appState.currentTitle,
+ titleAlignment = Alignment.CenterHorizontally,
backButton = isVisibleBackButton,
onBackIconClicked = appState::upPress
)
@@ -156,8 +155,8 @@ fun CodeApp(tipsEngine: TipsEngine) {
}
}
BiometricsBlockingView(modifier = Modifier.fillMaxSize(), biometricsState)
- TopBarContainer(appState)
- BottomBarContainer(appState)
+ TopBarContainer(appState.barMessages)
+ BottomBarContainer(appState.barMessages)
ConfirmationModals(Modifier.fillMaxSize())
}
}
@@ -181,7 +180,8 @@ private fun AppNavHost(content: @Composable () -> Unit) {
}
}
- }
+ },
+ onHide = com.getcode.services.manager.ModalManager::clear
) { sheetNav ->
combinedNavigator =
combinedNavigator?.apply { sheetNavigator = sheetNav } ?: CombinedNavigator(sheetNav)
@@ -206,19 +206,3 @@ private fun CrossfadeTransition(
transition = { fadeIn() togetherWith fadeOut() }
)
}
-
-internal data object MainRoot : Screen {
-
- override val key: ScreenKey = uniqueScreenKey
-
- private fun readResolve(): Any = this
-
- @Composable
- override fun Content() {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(CodeTheme.colors.background)
- )
- }
-}
diff --git a/app/src/main/java/com/getcode/CodeAppState.kt b/app/src/main/java/com/getcode/CodeAppState.kt
index 657eec04d..0218d83e3 100644
--- a/app/src/main/java/com/getcode/CodeAppState.kt
+++ b/app/src/main/java/com/getcode/CodeAppState.kt
@@ -6,15 +6,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import com.getcode.manager.BottomBarManager
-import com.getcode.manager.ModalManager
-import com.getcode.manager.TopBarManager
+import com.getcode.services.manager.ModalManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.AccessKeyLoginScreen
import com.getcode.navigation.screens.LoginPhoneVerificationScreen
import com.getcode.navigation.screens.LoginScreen
import com.getcode.navigation.screens.NamedScreen
+import com.getcode.ui.components.bars.BarManager
+import com.getcode.ui.components.bars.BarMessages
+import com.getcode.ui.components.bars.rememberBarManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -26,10 +27,11 @@ import kotlinx.coroutines.launch
fun rememberCodeAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navigator: CodeNavigator = LocalCodeNavigator.current,
+ barManager: BarManager = rememberBarManager(),
coroutineScope: CoroutineScope = rememberCoroutineScope()
) =
- remember(scaffoldState, navigator , coroutineScope) {
- CodeAppState(scaffoldState, navigator, coroutineScope)
+ remember(scaffoldState, navigator , barManager, coroutineScope) {
+ CodeAppState(scaffoldState, navigator, barManager, coroutineScope)
}
/**
@@ -39,21 +41,12 @@ fun rememberCodeAppState(
class CodeAppState(
val scaffoldState: ScaffoldState,
var navigator: CodeNavigator,
+ private val barManager: BarManager,
coroutineScope: CoroutineScope
) {
init {
coroutineScope.launch {
- TopBarManager.messages.collect { currentMessages ->
- topBarMessage.value = currentMessages.firstOrNull()
- }
- }
- coroutineScope.launch {
- BottomBarManager.messages.collect { currentMessages ->
- bottomBarMessage.value = currentMessages.firstOrNull()
- }
- }
- coroutineScope.launch {
- ModalManager.messages.collect { currentMessages ->
+ com.getcode.services.manager.ModalManager.messages.collect { currentMessages ->
modalMessage.value = currentMessages.firstOrNull()
}
}
@@ -88,9 +81,10 @@ class CodeAppState(
)
}
- val topBarMessage = MutableStateFlow(null)
- val bottomBarMessage = MutableStateFlow(null)
- val modalMessage = MutableStateFlow(null)
+ val barMessages: BarMessages
+ get() = barManager.barMessages
+
+ val modalMessage = MutableStateFlow(null)
fun upPress() {
if (navigator.pop().not()) {
diff --git a/app/src/main/java/com/getcode/Locals.kt b/app/src/main/java/com/getcode/Locals.kt
index c4b9541ee..4e0aa67c3 100644
--- a/app/src/main/java/com/getcode/Locals.kt
+++ b/app/src/main/java/com/getcode/Locals.kt
@@ -1,29 +1,19 @@
package com.getcode
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.painter.BitmapPainter
import com.getcode.analytics.AnalyticsService
import com.getcode.analytics.AnalyticsServiceNull
-import com.getcode.network.exchange.Exchange
-import com.getcode.network.exchange.ExchangeNull
import com.getcode.network.repository.BetaOptions
import com.getcode.ui.utils.BiometricsState
-import com.getcode.util.CurrencyUtils
import com.getcode.util.DeeplinkHandler
import com.getcode.util.PhoneUtils
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.utils.network.NetworkObserverStub
val LocalSession: ProvidableCompositionLocal = staticCompositionLocalOf { null }
val LocalAnalytics: ProvidableCompositionLocal = staticCompositionLocalOf { AnalyticsServiceNull() }
-val LocalNetworkObserver: ProvidableCompositionLocal = staticCompositionLocalOf { NetworkObserverStub() }
val LocalPhoneFormatter: ProvidableCompositionLocal = staticCompositionLocalOf { null }
-val LocalCurrencyUtils: ProvidableCompositionLocal = staticCompositionLocalOf { null }
-val LocalExchange: ProvidableCompositionLocal = staticCompositionLocalOf { ExchangeNull() }
val LocalDeeplinks: ProvidableCompositionLocal = staticCompositionLocalOf { null }
-val LocalTopBarPadding: ProvidableCompositionLocal = staticCompositionLocalOf { PaddingValues() }
val LocalBetaFlags: ProvidableCompositionLocal = staticCompositionLocalOf { BetaOptions.Defaults }
val LocalDownloadQrCode: ProvidableCompositionLocal = staticCompositionLocalOf { null }
val LocalBiometricsState: ProvidableCompositionLocal = staticCompositionLocalOf { BiometricsState() }
diff --git a/app/src/main/java/com/getcode/MainRoot.kt b/app/src/main/java/com/getcode/MainRoot.kt
new file mode 100644
index 000000000..19fbb200a
--- /dev/null
+++ b/app/src/main/java/com/getcode/MainRoot.kt
@@ -0,0 +1,30 @@
+package com.getcode
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import com.getcode.navigation.screens.ScanScreen
+import com.getcode.theme.CodeTheme
+
+internal data object MainRoot : Screen {
+
+ override val key: ScreenKey = uniqueScreenKey
+
+ private fun readResolve(): Any = this
+
+ @Composable
+ override fun Content() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(CodeTheme.colors.background)
+ )
+ }
+}
+
+typealias AppHomeScreen = ScanScreen
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/SessionController.kt b/app/src/main/java/com/getcode/SessionController.kt
index 53512c210..0b6062582 100644
--- a/app/src/main/java/com/getcode/SessionController.kt
+++ b/app/src/main/java/com/getcode/SessionController.kt
@@ -14,32 +14,22 @@ import com.getcode.analytics.AnalyticsManager
import com.getcode.analytics.AnalyticsService
import com.getcode.domain.CashLinkManager
import com.getcode.manager.AuthManager
-import com.getcode.manager.BottomBarManager
import com.getcode.manager.GiftCardManager
-import com.getcode.manager.MnemonicManager
-import com.getcode.manager.ModalManager
import com.getcode.manager.SessionManager
-import com.getcode.manager.TopBarManager
import com.getcode.model.BuyModuleFeature
import com.getcode.model.CameraGesturesFeature
-import com.getcode.model.CodePayload
import com.getcode.model.Currency
-import com.getcode.model.Domain
import com.getcode.model.Feature
-import com.getcode.model.Fiat
import com.getcode.model.FlippableTipCardFeature
import com.getcode.model.GalleryFeature
-import com.getcode.model.ID
import com.getcode.model.IntentMetadata
import com.getcode.model.InvertedDragZoomFeature
import com.getcode.model.Kin
import com.getcode.model.KinAmount
-import com.getcode.model.Kind
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
import com.getcode.model.RequestKinFeature
import com.getcode.model.SocialUser
import com.getcode.model.TwitterUser
-import com.getcode.model.Username
import com.getcode.model.notifications.NotificationType
import com.getcode.models.Bill
import com.getcode.models.BillState
@@ -47,13 +37,12 @@ import com.getcode.models.BillToast
import com.getcode.models.ConfirmationState
import com.getcode.models.DeepLinkRequest
import com.getcode.models.LoginConfirmation
-import com.getcode.models.PaymentConfirmation
+import com.getcode.models.PrivatePaymentConfirmation
import com.getcode.models.PaymentValuation
import com.getcode.models.SocialUserPaymentConfirmation
import com.getcode.models.amountFloored
import com.getcode.network.BalanceController
import com.getcode.network.NotificationCollectionHistoryController
-import com.getcode.network.ChatHistoryController
import com.getcode.network.TipController
import com.getcode.network.client.Client
import com.getcode.network.client.RemoteSendException
@@ -77,26 +66,30 @@ import com.getcode.network.repository.PaymentRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.ReceiveTransactionRepository
import com.getcode.network.repository.StatusRepository
-import com.getcode.network.repository.hexEncodedString
-import com.getcode.network.repository.toPublicKey
+import com.getcode.util.IntentUtils
+import com.getcode.utils.Kin
+import com.getcode.extensions.formatted
+import com.getcode.manager.BottomBarManager
+import com.getcode.manager.TopBarManager
+import com.getcode.services.model.CodePayload
+import com.getcode.model.Domain
+import com.getcode.services.model.Kind
+import com.getcode.services.model.payload.Username
+import com.getcode.model.toPublicKey
+import com.getcode.services.utils.catchSafely
+import com.getcode.services.utils.nonce
import com.getcode.solana.organizer.GiftCardAccount
import com.getcode.solana.organizer.Organizer
-import com.getcode.ui.components.PermissionResult
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.IntentUtils
-import com.getcode.util.Kin
-import com.getcode.util.formatted
+import com.getcode.ui.components.restrictions.RestrictionType
import com.getcode.util.permissions.PermissionChecker
+import com.getcode.util.permissions.PermissionResult
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.TraceType
-import com.getcode.utils.catchSafely
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.utils.nonce
+import com.getcode.utils.hexEncodedString
import com.getcode.utils.trace
-import com.getcode.vendor.Base58
import com.getcode.view.main.scanner.UiElement
import com.kik.kikx.kikcodes.implementation.KikCodeAnalyzer
import com.kik.kikx.models.ScannableKikCode
@@ -104,14 +97,12 @@ import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
@@ -127,6 +118,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import org.kin.sdk.base.tools.Base58
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
@@ -179,13 +171,7 @@ sealed interface SessionEvent {
data object PresentTipEntry : SessionEvent
data object RequestNotificationPermissions : SessionEvent
data class SendIntent(val intent: Intent) : SessionEvent
- data class OnChatPaidForSuccessfully(val intentId: ID, val user: SocialUser): SessionEvent
-}
-
-enum class RestrictionType {
- ACCESS_EXPIRED,
- FORCE_UPGRADE,
- TIMELOCK_UNLOCKED
+ data class OnChatPaidForSuccessfully(val intentId: com.getcode.model.ID, val user: SocialUser): SessionEvent
}
@SuppressLint("CheckResult")
@@ -196,22 +182,22 @@ class SessionController @Inject constructor(
private val paymentRepository: PaymentRepository,
private val balanceController: BalanceController,
private val historyController: NotificationCollectionHistoryController,
- private val chatHistoryController: ChatHistoryController,
private val tipController: TipController,
private val prefRepository: PrefRepository,
private val analytics: AnalyticsService,
private val authManager: AuthManager,
- private val networkObserver: NetworkConnectivityListener,
+ private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
private val resources: ResourceHelper,
private val vibrator: Vibrator,
- private val currencyUtils: CurrencyUtils,
+ private val currencyUtils: com.getcode.utils.CurrencyUtils,
private val exchange: Exchange,
private val giftCardManager: GiftCardManager,
- private val mnemonicManager: MnemonicManager,
+ private val mnemonicManager: com.getcode.services.manager.MnemonicManager,
private val cashLinkManager: CashLinkManager,
private val permissionChecker: PermissionChecker,
private val notificationManager: NotificationManagerCompat,
private val codeAnalyzer: KikCodeAnalyzer,
+ private val sessionManager: SessionManager,
appSettings: AppSettingsRepository,
betaFlagsRepository: BetaFlagsRepository,
features: FeatureRepository,
@@ -405,13 +391,6 @@ class SessionController @Inject constructor(
state.update { it.copy(notificationUnreadCount = count) }
}.launchIn(scope)
- chatHistoryController.unreadCount
- .distinctUntilChanged()
- .map { it }
- .onEach { count ->
- state.update { it.copy(chatUnreadCount = count) }
- }.launchIn(scope)
-
prefRepository.observeOrDefault(PrefsBool.LOG_SCAN_TIMES, false)
.flowOn(Dispatchers.IO)
.onEach { log ->
@@ -463,10 +442,6 @@ class SessionController @Inject constructor(
UiElement.GET_KIN
}
- if (betaOptions.conversationsEnabled) {
- actions += UiElement.CHAT
- }
-
actions += UiElement.BALANCE
return actions
@@ -559,7 +534,6 @@ class SessionController @Inject constructor(
}
private fun presentSend(data: List, bill: Bill, isVibrate: Boolean = false) {
- println("present send")
if (bill.didReceive) {
state.update {
val billState = it.billState
@@ -867,8 +841,8 @@ class SessionController @Inject constructor(
if (show) {
delay(400)
- ModalManager.showMessage(
- ModalManager.Message(
+ com.getcode.services.manager.ModalManager.showMessage(
+ com.getcode.services.manager.ModalManager.Message(
icon = R.drawable.ic_bell,
title = resources.getString(R.string.title_turnOnNotifications),
subtitle = resources.getString(R.string.subtitle_turnOnNotifications),
@@ -1146,7 +1120,7 @@ class SessionController @Inject constructor(
if (payload != null) {
code = payload
} else {
- val fiat = Fiat(currency = amount.rate.currency, amount = amount.fiat)
+ val fiat = com.getcode.model.Fiat(currency = amount.rate.currency, amount = amount.fiat)
code = CodePayload(
kind = Kind.RequestPayment,
@@ -1173,7 +1147,7 @@ class SessionController @Inject constructor(
if (isReceived) {
billState = billState.copy(
- paymentConfirmation = PaymentConfirmation(
+ privatePaymentConfirmation = PrivatePaymentConfirmation(
state = ConfirmationState.AwaitingConfirmation,
payload = code,
requestedAmount = amount,
@@ -1207,12 +1181,12 @@ class SessionController @Inject constructor(
// keep bill active while sending
cashLinkManager.cancelBillTimeout()
- val paymentConfirmation = state.value.billState.paymentConfirmation ?: return@launch
+ val paymentConfirmation = state.value.billState.privatePaymentConfirmation ?: return@launch
state.update {
val billState = it.billState
it.copy(
billState = billState.copy(
- paymentConfirmation = paymentConfirmation.copy(state = ConfirmationState.Sending)
+ privatePaymentConfirmation = paymentConfirmation.copy(state = ConfirmationState.Sending)
),
)
}
@@ -1226,11 +1200,11 @@ class SessionController @Inject constructor(
state.update {
val billState = it.billState
- val confirmation = it.billState.paymentConfirmation ?: return@update it
+ val confirmation = it.billState.privatePaymentConfirmation ?: return@update it
it.copy(
billState = billState.copy(
- paymentConfirmation = confirmation.copy(state = ConfirmationState.Sent),
+ privatePaymentConfirmation = confirmation.copy(state = ConfirmationState.Sent),
),
)
}
@@ -1253,7 +1227,7 @@ class SessionController @Inject constructor(
billState = uiModel.billState.copy(
bill = null,
showToast = false,
- paymentConfirmation = null,
+ privatePaymentConfirmation = null,
toast = null,
valuation = null,
primaryAction = null,
@@ -1265,7 +1239,7 @@ class SessionController @Inject constructor(
}
private fun cancelPayment(rejected: Boolean, ignoreRedirect: Boolean = false) {
- val paymentRendezous = state.value.billState.paymentConfirmation
+ val paymentRendezous = state.value.billState.privatePaymentConfirmation
val bill = state.value.billState.bill ?: return
val amount = bill.amount
val request = bill.metadata.request
@@ -1284,7 +1258,7 @@ class SessionController @Inject constructor(
presentationStyle = PresentationStyle.Slide,
billState = it.billState.copy(
bill = null,
- paymentConfirmation = null,
+ privatePaymentConfirmation = null,
valuation = null,
primaryAction = null,
secondaryAction = null,
@@ -1325,7 +1299,7 @@ class SessionController @Inject constructor(
}
fun rejectPayment(ignoreRedirect: Boolean = false) {
- val payload = state.value.billState.paymentConfirmation?.payload
+ val payload = state.value.billState.privatePaymentConfirmation?.payload
cancelPayment(true, ignoreRedirect)
payload ?: return
@@ -1450,7 +1424,7 @@ class SessionController @Inject constructor(
billState = it.billState.copy(
bill = null,
showToast = false,
- paymentConfirmation = null,
+ privatePaymentConfirmation = null,
loginConfirmation = null,
toast = null,
valuation = null,
@@ -1725,8 +1699,8 @@ class SessionController @Inject constructor(
cancelRemoteSend(giftCard, amount)
cancelSend(style = PresentationStyle.Slide)
},
- onClose = {
- if (it == null) {
+ onClose = { fromAction ->
+ if (!fromAction) {
cancelSend(style = PresentationStyle.Pop)
vibrator.vibrate()
}
@@ -1743,6 +1717,7 @@ class SessionController @Inject constructor(
if (request != null) {
when {
request.paymentRequest != null -> {
+ val payment = request.paymentRequest!!
scope.launch {
if (state.value.balance == null) {
balanceController.fetchBalanceSuspend()
@@ -1753,9 +1728,9 @@ class SessionController @Inject constructor(
it.copy(balance = amount)
}
}
- val fiat = request.paymentRequest.fiat
+ val fiat = payment.fiat
val kind =
- if (request.paymentRequest.fees.isEmpty()) Kind.RequestPayment else Kind.RequestPaymentV2
+ if (payment.fees.isEmpty()) Kind.RequestPayment else Kind.RequestPaymentV2
val payload = CodePayload(
kind = kind,
value = fiat,
@@ -1789,9 +1764,10 @@ class SessionController @Inject constructor(
}
request.tipRequest != null -> {
+ val tip = request.tipRequest!!
val payload = CodePayload(
kind = Kind.Tip,
- value = Username(request.tipRequest.username)
+ value = Username(tip.username)
)
if (scannedRendezvous.contains(payload.rendezvous.publicKey)) {
@@ -1805,7 +1781,8 @@ class SessionController @Inject constructor(
}
request.imageRequest != null -> {
- onImageSelected(request.imageRequest.uri)
+ val image = request.imageRequest!!
+ onImageSelected(image.uri)
}
}
}
diff --git a/app/src/main/java/com/getcode/TopLevelViewModel.kt b/app/src/main/java/com/getcode/TopLevelViewModel.kt
index 5bceb4067..c6816d19d 100644
--- a/app/src/main/java/com/getcode/TopLevelViewModel.kt
+++ b/app/src/main/java/com/getcode/TopLevelViewModel.kt
@@ -4,26 +4,22 @@ import android.app.Activity
import androidx.lifecycle.viewModelScope
import com.getcode.manager.AuthManager
import com.getcode.manager.TopBarManager
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
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.PrefRepository
import com.getcode.util.resources.ResourceHelper
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
diff --git a/app/src/main/java/com/getcode/domain/CashLinkManager.kt b/app/src/main/java/com/getcode/domain/CashLinkManager.kt
index d60161042..325b33df8 100644
--- a/app/src/main/java/com/getcode/domain/CashLinkManager.kt
+++ b/app/src/main/java/com/getcode/domain/CashLinkManager.kt
@@ -1,6 +1,5 @@
package com.getcode.domain
-import com.getcode.ed25519.Ed25519
import com.getcode.ed25519.Ed25519.KeyPair
import com.getcode.model.KinAmount
import com.getcode.network.repository.SendTransactionRepository
diff --git a/app/src/main/java/com/getcode/inject/ApiModule.kt b/app/src/main/java/com/getcode/inject/ApiModule.kt
index f503bba84..df920b4fc 100644
--- a/app/src/main/java/com/getcode/inject/ApiModule.kt
+++ b/app/src/main/java/com/getcode/inject/ApiModule.kt
@@ -2,57 +2,30 @@ package com.getcode.inject
import android.content.Context
import com.getcode.BuildConfig
-import com.getcode.R
-import com.getcode.analytics.AnalyticsService
-import com.getcode.annotations.DevManagedChannel
import com.getcode.api.KadoApi
-import com.getcode.manager.MnemonicManager
+import com.getcode.model.Currency
import com.getcode.model.CurrencyCode
-import com.getcode.model.PrefsString
-import com.getcode.network.BalanceController
-import com.getcode.network.PrivacyMigration
-import com.getcode.network.api.CurrencyApi
-import com.getcode.network.api.TransactionApiV2
-import com.getcode.network.client.AccountService
-import com.getcode.network.client.Client
-import com.getcode.network.client.TransactionReceiver
-import com.getcode.network.core.NetworkOracle
-import com.getcode.network.core.NetworkOracleImpl
-import com.getcode.network.exchange.CodeExchange
-import com.getcode.network.exchange.Exchange
-import com.getcode.network.repository.AccountRepository
-import com.getcode.network.repository.BalanceRepository
-import com.getcode.network.repository.IdentityRepository
-import com.getcode.network.repository.MessagingRepository
import com.getcode.network.repository.PrefRepository
-import com.getcode.network.repository.TransactionRepository
-import com.getcode.network.service.ChatServiceV1
+import com.getcode.services.R
+import com.getcode.services.db.CurrencyProvider
+import com.getcode.services.model.PrefsString
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
+import com.getcode.utils.CurrencyUtils
import com.mixpanel.android.mpmetrics.MixpanelAPI
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
-import io.grpc.ManagedChannel
-import io.grpc.android.AndroidChannelBuilder
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
-import org.kin.sdk.base.network.api.agora.OkHttpChannelBuilderForcedTls12
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
-import retrofit2.create
-import java.util.concurrent.TimeUnit
import javax.inject.Named
import javax.inject.Singleton
@@ -60,11 +33,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object ApiModule {
- @Provides
- fun provideNetworkOracle(): NetworkOracle {
- return NetworkOracleImpl()
- }
-
@Singleton
@Provides
fun provideCompositeDisposable(): CompositeDisposable {
@@ -74,35 +42,6 @@ object ApiModule {
@Provides
fun provideScheduler(): Scheduler = Schedulers.io()
- @Singleton
- @Provides
- fun provideManagedChannel(@ApplicationContext context: Context): ManagedChannel {
- val TLS_PORT = 443
- val PROD_URL = "api.codeinfra.net"
-
- return AndroidChannelBuilder
- .usingBuilder(OkHttpChannelBuilderForcedTls12.forAddress(PROD_URL, TLS_PORT))
- .context(context)
- .userAgent("Code/Android/${BuildConfig.VERSION_NAME}")
- .keepAliveTime(4, TimeUnit.MINUTES)
- .build()
- }
-
- @Singleton
- @Provides
- @DevManagedChannel
- fun provideDevManagedChannel(@ApplicationContext context: Context): ManagedChannel {
- val TLS_PORT = 443
- val DEV_URL = "api.codeinfra.dev"
-
- return AndroidChannelBuilder
- .usingBuilder(OkHttpChannelBuilderForcedTls12.forAddress(DEV_URL, TLS_PORT))
- .context(context)
- .userAgent("Code/Android/${BuildConfig.VERSION_NAME}")
- .keepAliveTime(4, TimeUnit.MINUTES)
- .build()
- }
-
@Singleton
@Provides
fun provideAccountAuthenticator(
@@ -150,123 +89,29 @@ object ApiModule {
@Singleton
@Provides
- fun provideBalanceRepository(
- ): BalanceRepository {
- return BalanceRepository()
- }
-
- @Singleton
- @Provides
- fun provideBalanceController(
- exchange: Exchange,
- balanceRepository: BalanceRepository,
- transactionRepository: TransactionRepository,
- accountRepository: AccountRepository,
- privacyMigration: PrivacyMigration,
- transactionReceiver: TransactionReceiver,
- networkObserver: NetworkConnectivityListener,
- resources: ResourceHelper,
+ fun providesCurrencyProvider(
+ prefRepository: PrefRepository,
currencyUtils: CurrencyUtils,
- ): BalanceController {
- return BalanceController(
- exchange = exchange,
- balanceRepository = balanceRepository,
- transactionRepository = transactionRepository,
- accountRepository = accountRepository,
- privacyMigration = privacyMigration,
- transactionReceiver = transactionReceiver,
- networkObserver = networkObserver,
- getCurrencyFromCode = {
- it?.name?.let(currencyUtils::getCurrency)
- },
- suffix = { currency ->
- if (currency?.code == CurrencyCode.KIN.name) {
- ""
- } else {
- resources.getString(R.string.core_ofKin)
- }
- }
- )
- }
-
- @Singleton
- @Provides
- fun providesExchange(
- currencyService: CurrencyService,
locale: LocaleHelper,
- currencyUtils: CurrencyUtils,
- prefRepository: PrefRepository,
- ): Exchange = CodeExchange(
- currencyService = currencyService,
- prefs = prefRepository,
- preferredCurrency = {
+ resources: ResourceHelper,
+ ): CurrencyProvider = object : CurrencyProvider {
+ override suspend fun preferredCurrency(): Currency? {
val preferredCurrencyCode = prefRepository.get(
PrefsString.KEY_LOCAL_CURRENCY,
""
).takeIf { it.isNotEmpty() }
-
val preferredCurrency = preferredCurrencyCode?.let { currencyUtils.getCurrency(it) }
- preferredCurrency ?: locale.getDefaultCurrency()
- },
- defaultCurrency = { locale.getDefaultCurrency() }
- )
-
- @Singleton
- @Provides
- fun provideClient(
- identityRepository: IdentityRepository,
- transactionRepository: TransactionRepository,
- messagingRepository: MessagingRepository,
- accountRepository: AccountRepository,
- accountService: AccountService,
- balanceController: BalanceController,
- analytics: AnalyticsService,
- prefRepository: PrefRepository,
- transactionReceiver: TransactionReceiver,
- exchange: Exchange,
- networkObserver: NetworkConnectivityListener,
- chatServiceV1: ChatServiceV1,
- chatServiceV2: ChatServiceV2,
- deviceService: DeviceService,
- mnemonicManager: MnemonicManager,
- ): Client {
- return Client(
- identityRepository,
- transactionRepository,
- messagingRepository,
- balanceController,
- accountRepository,
- accountService,
- analytics,
- prefRepository,
- exchange,
- transactionReceiver,
- networkObserver,
- chatServiceV1,
- chatServiceV2,
- deviceService,
- mnemonicManager
- )
- }
+ return preferredCurrency ?: locale.getDefaultCurrency()
+ }
- @Singleton
- @Provides
- fun providePrivacyMigration(
- transactionRepository: TransactionRepository,
- analytics: AnalyticsService,
- ): PrivacyMigration {
- return PrivacyMigration(
- transactionRepository,
- analytics
- )
- }
+ override suspend fun defaultCurrency(): Currency? = locale.getDefaultCurrency()
- @Singleton
- @Provides
- fun provideTransactionRepository(
- @ApplicationContext context: Context,
- transactionApi: TransactionApiV2,
- ): TransactionRepository {
- return TransactionRepository(transactionApi = transactionApi, context = context)
+ override fun suffix(currency: Currency?): String {
+ return if (currency?.code == CurrencyCode.KIN.name) {
+ ""
+ } else {
+ resources.getString(R.string.core_ofKin)
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/inject/AppModule.kt b/app/src/main/java/com/getcode/inject/AppModule.kt
index af897a06c..58552536f 100644
--- a/app/src/main/java/com/getcode/inject/AppModule.kt
+++ b/app/src/main/java/com/getcode/inject/AppModule.kt
@@ -14,15 +14,15 @@ import com.getcode.analytics.AnalyticsManager
import com.getcode.analytics.AnalyticsService
import com.getcode.util.AndroidLocale
import com.getcode.util.AndroidPermissions
-import com.getcode.util.AndroidResources
-import com.getcode.util.Api25Vibrator
-import com.getcode.util.Api26Vibrator
-import com.getcode.util.Api31Vibrator
-import com.getcode.util.CurrencyUtils
+import com.getcode.util.vibration.Api25Vibrator
+import com.getcode.util.vibration.Api26Vibrator
+import com.getcode.util.vibration.Api31Vibrator
import com.getcode.util.locale.LocaleHelper
import com.getcode.util.permissions.PermissionChecker
+import com.getcode.util.resources.AndroidResources
import com.getcode.util.resources.ResourceHelper
import com.getcode.util.vibration.Vibrator
+import com.getcode.utils.CurrencyUtils
import com.getcode.utils.network.Api24NetworkObserver
import com.getcode.utils.network.Api29NetworkObserver
import com.getcode.utils.network.NetworkConnectivityListener
@@ -50,11 +50,6 @@ object AppModule {
currencyUtils: CurrencyUtils,
): LocaleHelper = AndroidLocale(context, currencyUtils)
- @Provides
- fun providesAnalyticsService(
- mixpanelAPI: MixpanelAPI
- ): AnalyticsService = AnalyticsManager(mixpanelAPI)
-
@Provides
fun providesWifiManager(
@ApplicationContext context: Context,
@@ -79,9 +74,16 @@ object AppModule {
wifiManager: WifiManager
): NetworkConnectivityListener = when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.N .. Build.VERSION_CODES.P -> {
- Api24NetworkObserver(wifiManager, connectivityManager, telephonyManager)
+ Api24NetworkObserver(
+ wifiManager,
+ connectivityManager,
+ telephonyManager
+ )
}
- else -> Api29NetworkObserver(connectivityManager, telephonyManager)
+ else -> Api29NetworkObserver(
+ connectivityManager,
+ telephonyManager
+ )
}
@Provides
@@ -96,7 +98,7 @@ object AppModule {
@Provides
- fun providesCNotificationManager(
+ fun providesNotificationManager(
@ApplicationContext context: Context
): NotificationManagerCompat = NotificationManagerCompat.from(context)
diff --git a/app/src/main/java/com/getcode/inject/DataModule.kt b/app/src/main/java/com/getcode/inject/DataModule.kt
deleted file mode 100644
index b7247b164..000000000
--- a/app/src/main/java/com/getcode/inject/DataModule.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.getcode.inject
-
-import com.getcode.mapper.ConversationMapper
-import com.getcode.mapper.ConversationMessageWithContentMapper
-import com.getcode.network.ConversationController
-import com.getcode.network.ConversationStreamController
-import com.getcode.network.ChatHistoryController
-import com.getcode.network.TipController
-import com.getcode.network.exchange.Exchange
-import com.getcode.network.service.ChatServiceV2
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-object DataModule {
-
- @Provides
- @Singleton
- fun providesConversationController(
- historyController: ChatHistoryController,
- chatServiceV2: ChatServiceV2,
- exchange: Exchange,
- conversationMapper: ConversationMapper,
- messageWithContentMapper: ConversationMessageWithContentMapper,
- tipController: TipController,
- ): ConversationController = ConversationStreamController(
- historyController = historyController,
- exchange = exchange,
- chatService = chatServiceV2,
- conversationMapper = conversationMapper,
- messageWithContentMapper = messageWithContentMapper,
- tipController = tipController
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/manager/AuthManager.kt b/app/src/main/java/com/getcode/manager/AuthManager.kt
index 4bceb47c9..bd1046505 100644
--- a/app/src/main/java/com/getcode/manager/AuthManager.kt
+++ b/app/src/main/java/com/getcode/manager/AuthManager.kt
@@ -6,31 +6,31 @@ import com.bugsnag.android.Bugsnag
import com.getcode.BuildConfig
import com.getcode.analytics.AnalyticsService
import com.getcode.crypt.MnemonicPhrase
-import com.getcode.db.Database
+import com.getcode.db.CodeAppDatabase
import com.getcode.db.InMemoryDao
import com.getcode.model.AirdropType
-import com.getcode.model.PrefsBool
-import com.getcode.model.PrefsString
+import com.getcode.services.model.PrefsBool
+import com.getcode.services.model.PrefsString
import com.getcode.model.description
import com.getcode.network.BalanceController
import com.getcode.network.NotificationCollectionHistoryController
-import com.getcode.network.ChatHistoryController
import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BetaFlagsRepository
import com.getcode.network.repository.IdentityRepository
import com.getcode.network.repository.PhoneRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.PushRepository
-import com.getcode.network.repository.encodeBase64
-import com.getcode.network.repository.getPublicKeyBase58
import com.getcode.network.repository.isMock
+import com.getcode.services.db.Database
+import com.getcode.services.utils.installationId
+import com.getcode.services.utils.makeE164
+import com.getcode.services.utils.token
import com.getcode.util.AccountUtils
import com.getcode.utils.ErrorUtils
import com.getcode.utils.TraceType
-import com.getcode.utils.installationId
-import com.getcode.utils.makeE164
+import com.getcode.utils.encodeBase64
+import com.getcode.utils.getPublicKeyBase58
import com.getcode.utils.trace
-import com.getcode.utils.token
import com.google.firebase.Firebase
import com.google.firebase.installations.installations
import com.google.firebase.messaging.FirebaseMessaging
@@ -60,10 +60,9 @@ class AuthManager @Inject constructor(
private val exchange: Exchange,
private val balanceController: BalanceController,
private val notificationCollectionHistory: NotificationCollectionHistoryController,
- private val chatHistory: ChatHistoryController,
private val inMemoryDao: InMemoryDao,
private val analytics: AnalyticsService,
- private val mnemonicManager: MnemonicManager,
+ private val mnemonicManager: com.getcode.services.manager.MnemonicManager,
private val mixpanelAPI: MixpanelAPI
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private var softLoginDisabled: Boolean = false
@@ -104,8 +103,9 @@ class AuthManager @Inject constructor(
return Single.create {
softLoginDisabled = true
- if (!Database.isOpen()) {
- Database.init(context, entropyB64)
+ if (!CodeAppDatabase.isOpen()) {
+ CodeAppDatabase.init(context, entropyB64)
+ Database.register(CodeAppDatabase.requireInstance())
}
val originalSessionState = SessionManager.authState.value
@@ -137,8 +137,9 @@ class AuthManager @Inject constructor(
return Single.create {
if (!isSoftLogin) softLoginDisabled = true
- if (!Database.isOpen()) {
- Database.init(context, entropyB64)
+ if (!CodeAppDatabase.isOpen()) {
+ CodeAppDatabase.init(context, entropyB64)
+ Database.register(CodeAppDatabase.requireInstance())
}
val originalSessionState = SessionManager.authState.value
@@ -291,7 +292,6 @@ class AuthManager @Inject constructor(
launch { savePrefs(phone!!, user!!) }
launch { exchange.fetchRatesIfNeeded() }
launch { notificationCollectionHistory.fetch() }
- launch { chatHistory.fetch() }
}
}
@@ -312,7 +312,7 @@ class AuthManager @Inject constructor(
Database.close()
notificationCollectionHistory.reset()
inMemoryDao.clear()
- Database.delete(context)
+ launch { Database.delete(context) }
if (!BuildConfig.DEBUG) Bugsnag.setUser(null, null, null)
}
diff --git a/app/src/main/java/com/getcode/mapper/AppSettingsMapper.kt b/app/src/main/java/com/getcode/mapper/AppSettingsMapper.kt
index f33e2d44d..8657037e7 100644
--- a/app/src/main/java/com/getcode/mapper/AppSettingsMapper.kt
+++ b/app/src/main/java/com/getcode/mapper/AppSettingsMapper.kt
@@ -2,10 +2,11 @@ package com.getcode.mapper
import androidx.biometric.BiometricManager
import com.getcode.R
-import com.getcode.model.APP_SETTINGS
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.APP_SETTINGS
+import com.getcode.services.model.PrefsBool
import com.getcode.models.SettingItem
import com.getcode.network.repository.AppSettings
+import com.getcode.services.mapper.SuspendMapper
import javax.inject.Inject
class AppSettingsMapper @Inject constructor(
diff --git a/app/src/main/java/com/getcode/models/SettingsItem.kt b/app/src/main/java/com/getcode/models/SettingsItem.kt
index 7e563196d..b794316a4 100644
--- a/app/src/main/java/com/getcode/models/SettingsItem.kt
+++ b/app/src/main/java/com/getcode/models/SettingsItem.kt
@@ -1,6 +1,6 @@
package com.getcode.models
-import com.getcode.model.AppSetting
+import com.getcode.services.model.AppSetting
data class SettingItem(
val type: AppSetting,
diff --git a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt b/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt
deleted file mode 100644
index d1b1fbaf5..000000000
--- a/app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt
+++ /dev/null
@@ -1,216 +0,0 @@
-package com.getcode.navigation.screens
-
-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.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredWidth
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-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 com.getcode.R
-import com.getcode.model.ID
-import com.getcode.model.TwitterUser
-import com.getcode.model.chat.Reference
-import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.chat.UserAvatar
-import com.getcode.ui.utils.getActivityScopedViewModel
-import com.getcode.util.formatDateRelatively
-import com.getcode.view.main.balance.BalanceSheetViewModel
-import com.getcode.view.main.chat.conversation.ConversationScreen
-import com.getcode.view.main.chat.conversation.ConversationViewModel
-import com.getcode.view.main.chat.create.byusername.ChatByUsernameScreen
-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.onEach
-import kotlinx.parcelize.IgnoredOnParcel
-import kotlinx.parcelize.Parcelize
-import kotlinx.parcelize.RawValue
-
-@Parcelize
-data object ChatListModal: ChatGraph, ModalRoot {
-
- @IgnoredOnParcel
- override val key: ScreenKey = uniqueScreenKey
-
- override val name: String
- @Composable get() = stringResource(id = R.string.title_chat)
-
- @Composable
- override fun Content() {
- val navigator = LocalCodeNavigator.current
- val viewModel = getActivityScopedViewModel()
- ModalContainer(
- closeButtonEnabled = { it is ChatListModal },
- ) {
- ChatListScreen(viewModel)
- }
-
- LifecycleEffect(
- onStarted = {
- val disposedScreen = navigator.lastItem
- if (disposedScreen !is BalanceModal) {
- viewModel.dispatchEvent(ChatListViewModel.Event.OnOpened)
- }
- }
- )
- }
-}
-
-@Parcelize
-data object ChatByUsernameScreen: ChatGraph, ModalContent {
- @IgnoredOnParcel
- override val key: ScreenKey = uniqueScreenKey
-
- override val name: String
- @Composable get() = stringResource(id = R.string.title_whatsTheirUsername)
-
- @Composable
- override fun Content() {
- ModalContainer(
- backButtonEnabled = { it is ChatByUsernameScreen },
- ) {
- ChatByUsernameScreen(getViewModel())
- }
- }
-}
-
-@Parcelize
-data class ConversationScreen(
- val user: @RawValue TwitterUser? = null,
- val chatId: ID? = null,
- val intentId: ID? = null
-) : AppScreen(), ChatGraph, ModalContent {
- @IgnoredOnParcel
- override val key: ScreenKey = uniqueScreenKey
-
- @Composable
- override fun Content() {
- val navigator = LocalCodeNavigator.current
- val vm = getViewModel()
- val state by vm.stateFlow.collectAsState()
-
- ModalContainer(
- title = {
- Row(
- modifier = Modifier
- .padding(start = CodeTheme.dimens.staticGrid.x6)
- .align(Alignment.CenterStart),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2)
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy((-8).dp)
- ) {
- val imageModifier = Modifier
- .padding(start = CodeTheme.dimens.grid.x7)
- .size(CodeTheme.dimens.staticGrid.x6)
- .clip(CircleShape)
-
- state.users.fastForEachIndexed { index, user ->
- UserAvatar(
- modifier = imageModifier
- .zIndex((state.users.size - index).toFloat()),
- data = if (user.isRevealed) {
- user.imageUrl
- } else {
- user.memberId
- }
- )
- }
-
- if (state.users.isEmpty()) {
- Spacer(modifier = Modifier.requiredWidth(CodeTheme.dimens.grid.x3))
- }
- }
-
- Column {
- val title = state.users.mapNotNull { it.username }
- .joinToString()
- .takeIf { it.isNotEmpty() } ?: "Anonymous Tipper"
- Text(
- text = title,
- style = CodeTheme.typography.screenTitle
- )
- state.lastSeen?.let {
- Text(
- text = "Last seen ${it.formatDateRelatively()}",
- style = CodeTheme.typography.caption,
- color = CodeTheme.colors.textSecondary,
- )
- }
- }
- }
-
- },
- backButtonEnabled = { it is ConversationScreen },
- onBackClicked = {
- navigator.popUntil { it is ChatListModal }
- }
- ) {
- val messages = vm.messages.collectAsLazyPagingItems()
- ConversationScreen(state, messages, vm::dispatchEvent)
- }
-
- LaunchedEffect(vm) {
- vm.eventFlow
- .filterIsInstance()
- .onEach {
- navigator.push(EnterTipModal(isInChat = true))
- }.launchIn(this)
- }
-
- val context = LocalContext.current
- LaunchedEffect(vm) {
- vm.eventFlow
- .filterIsInstance()
- .onEach {
- if (it.show) {
- Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
- }
- if (it.fatal) {
- navigator.popAll()
- }
- }.launchIn(this)
- }
-
- LaunchedEffect(user) {
- if (user != null) {
- vm.dispatchEvent(
- ConversationViewModel.Event.OnTwitterUserChanged(user)
- )
- }
- }
-
- LaunchedEffect(chatId) {
- if (chatId != null) {
- vm.dispatchEvent(
- ConversationViewModel.Event.OnChatIdChanged(chatId)
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/navigation/screens/LoginScreen.kt b/app/src/main/java/com/getcode/navigation/screens/LoginScreen.kt
new file mode 100644
index 000000000..2c7adb842
--- /dev/null
+++ b/app/src/main/java/com/getcode/navigation/screens/LoginScreen.kt
@@ -0,0 +1,44 @@
+package com.getcode.navigation.screens
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.hilt.getViewModel
+import com.getcode.LocalAnalytics
+import com.getcode.R
+import com.getcode.analytics.Action
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.view.login.LoginHome
+import com.getcode.view.login.SeedDeepLink
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class LoginScreen(val seed: String? = null) : LoginGraph {
+ @IgnoredOnParcel
+ override val key: ScreenKey = uniqueScreenKey
+
+ override val name: String
+ @Composable get() = stringResource(id = R.string.action_logIn)
+
+ @Composable
+ override fun Content() {
+ val navigator = LocalCodeNavigator.current
+ val analytics = LocalAnalytics.current
+
+ if (seed != null) {
+ SeedDeepLink(getViewModel(), seed)
+ } else {
+ LoginHome(
+ createAccount = {
+ analytics.action(Action.CreateAccount)
+ navigator.push(LoginPhoneVerificationScreen(isNewAccount = true))
+ },
+ login = {
+ navigator.push(AccessKeyLoginScreen())
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/navigation/screens/LoginScreens.kt b/app/src/main/java/com/getcode/navigation/screens/LoginScreens.kt
index 1239ec72e..ef730ac11 100644
--- a/app/src/main/java/com/getcode/navigation/screens/LoginScreens.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/LoginScreens.kt
@@ -7,55 +7,21 @@ import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getViewModel
-import com.getcode.LocalAnalytics
import com.getcode.R
-import com.getcode.analytics.Action
-import com.getcode.analytics.AnalyticsManager
import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.ui.utils.getStackScopedViewModel
+import com.getcode.navigation.extensions.getStackScopedViewModel
import com.getcode.view.login.AccessKey
import com.getcode.view.login.AccessKeyViewModel
import com.getcode.view.login.CameraPermission
-import com.getcode.view.login.LoginHome
import com.getcode.view.login.NotificationPermission
import com.getcode.view.login.PhoneConfirm
import com.getcode.view.login.PhoneVerify
import com.getcode.view.login.PhoneVerifyViewModel
-import com.getcode.view.login.SeedDeepLink
import com.getcode.view.login.SeedInput
import com.getcode.view.login.SeedInputViewModel
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
-@Parcelize
-data class LoginScreen(val seed: String? = null) : LoginGraph {
- @IgnoredOnParcel
- override val key: ScreenKey = uniqueScreenKey
-
- override val name: String
- @Composable get() = stringResource(id = R.string.action_logIn)
-
- @Composable
- override fun Content() {
- val navigator = LocalCodeNavigator.current
- val analytics = LocalAnalytics.current
-
- if (seed != null) {
- SeedDeepLink(getViewModel(), seed)
- } else {
- LoginHome(
- createAccount = {
- analytics.action(Action.CreateAccount)
- navigator.push(LoginPhoneVerificationScreen(isNewAccount = true))
- },
- login = {
- navigator.push(AccessKeyLoginScreen())
- }
- )
- }
- }
-}
-
@Parcelize
data class LoginPhoneVerificationScreen(
val arguments: LoginArgs = LoginArgs()
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 cc11ddc3d..ae93e6c67 100644
--- a/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/MainScreens.kt
@@ -21,13 +21,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.model.ID
-import com.getcode.model.chat.Reference
import com.getcode.models.DeepLinkRequest
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.ui.components.SheetTitleDefaults
import com.getcode.ui.components.chat.utils.localized
import com.getcode.ui.utils.RepeatOnLifecycle
-import com.getcode.ui.utils.getActivityScopedViewModel
+import com.getcode.navigation.extensions.getActivityScopedViewModel
import com.getcode.utils.trace
import com.getcode.view.download.ShareDownloadScreen
import com.getcode.view.main.account.AccountHome
@@ -39,10 +38,8 @@ import com.getcode.view.main.chat.NotificationCollectionViewModel
import com.getcode.view.main.giveKin.GiveKinScreen
import com.getcode.view.main.requestKin.RequestKinScreen
import com.getcode.view.main.scanner.ScanScreen
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.IgnoredOnParcel
@@ -50,11 +47,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class ScanScreen(
- val seed: String? = null,
+ override val seed: String? = null,
val cashLink: String? = null,
@IgnoredOnParcel
val request: DeepLinkRequest? = null,
-) : AppScreen(), MainGraph {
+) : AppScreen(), MainScreen, MainGraph {
@IgnoredOnParcel
override val key: ScreenKey = uniqueScreenKey
@@ -279,7 +276,6 @@ data class NotificationCollectionScreen(val collectionId: ID) : MainGraph, Modal
override fun Content() {
val vm = getViewModel()
val state by vm.stateFlow.collectAsState()
- val navigator = LocalCodeNavigator.current
ModalContainer(
titleString = { state.title.localized },
@@ -289,16 +285,6 @@ data class NotificationCollectionScreen(val collectionId: ID) : MainGraph, Modal
ChatScreen(state = state, messages = messages, dispatch = vm::dispatchEvent)
}
- LaunchedEffect(vm) {
- vm.eventFlow
- .filterIsInstance()
- .map { it.reference }
- .filterIsInstance()
- .map { it.id }
- .onEach { navigator.push(ConversationScreen(intentId = it)) }
- .launchIn(this)
- }
-
LaunchedEffect(collectionId) {
vm.dispatchEvent(NotificationCollectionViewModel.Event.OnChatIdChanged(collectionId))
}
diff --git a/app/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt b/app/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt
index 3ab21a9fb..0e4745813 100644
--- a/app/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt
@@ -3,27 +3,23 @@ package com.getcode.navigation.screens
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import cafe.adriel.voyager.core.screen.Screen
-import com.getcode.manager.ModalManager
-import com.getcode.theme.BrandLight
+import com.getcode.navigation.modal.ModalHeightMetric
+import com.getcode.services.manager.ModalManager
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.utils.addIf
fun buildMessageContent(
@@ -55,7 +51,7 @@ private data class ModalContainerMessage(
message.icon?.let { imageResId ->
Box(
modifier = Modifier
- .background(BrandLight, CircleShape),
+ .background(CodeTheme.colors.brandLight, CircleShape),
contentAlignment = Alignment.Center
) {
Image(
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 540d55041..29412d1c2 100644
--- a/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/ModalScreens.kt
@@ -14,13 +14,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.analytics.Action
-import com.getcode.analytics.AnalyticsManager
import com.getcode.analytics.AnalyticsScreenWatcher
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.SheetTitleDefaults
-import com.getcode.ui.utils.getActivityScopedViewModel
-import com.getcode.ui.utils.getStackScopedViewModel
+import com.getcode.navigation.extensions.getActivityScopedViewModel
+import com.getcode.navigation.extensions.getStackScopedViewModel
import com.getcode.view.login.PhoneConfirm
import com.getcode.view.login.PhoneVerify
import com.getcode.view.login.PhoneVerifyViewModel
@@ -44,7 +43,7 @@ import com.getcode.view.main.getKin.KadoWebScreen
import com.getcode.view.main.tip.ConnectAccountScreen
import com.getcode.view.main.tip.EnterTipScreen
import com.getcode.view.main.tip.IdentityConnectionReason
-import com.getcode.view.main.tip.TipConnectViewModel
+import com.getcode.view.main.tip.ConnectAccountViewModel
import com.kevinnzou.web.rememberWebViewNavigator
import com.kevinnzou.web.rememberWebViewState
import kotlinx.parcelize.IgnoredOnParcel
@@ -458,15 +457,16 @@ data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot {
@Parcelize
data class ConnectAccount(
- private val reason: IdentityConnectionReason = IdentityConnectionReason.TipCard,
+ val reason: IdentityConnectionReason = IdentityConnectionReason.TipCard,
) : MainGraph, ModalContent {
+
@IgnoredOnParcel
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val navigator = LocalCodeNavigator.current
- val viewModel = getViewModel()
+ val viewModel = getViewModel()
when (reason) {
IdentityConnectionReason.TipCard -> {
ModalContainer(
@@ -494,13 +494,17 @@ data class ConnectAccount(
ConnectAccountScreen(viewModel)
}
}
+
+ IdentityConnectionReason.Login -> {
+ ConnectAccountScreen(viewModel)
+ }
}
LaunchedEffect(viewModel, reason) {
- viewModel.dispatchEvent(TipConnectViewModel.Event.OnReasonChanged(reason))
+ viewModel.dispatchEvent(ConnectAccountViewModel.Event.OnReasonChanged(reason))
}
- if (reason == IdentityConnectionReason.TipCard) {
+ if (reason == IdentityConnectionReason.TipCard || reason == IdentityConnectionReason.Login) {
AnalyticsScreenWatcher(action = Action.OpenConnectAccount)
}
}
diff --git a/app/src/main/java/com/getcode/navigation/screens/Modals.kt b/app/src/main/java/com/getcode/navigation/screens/Modals.kt
index beb2cfea3..12b2be7ec 100644
--- a/app/src/main/java/com/getcode/navigation/screens/Modals.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/Modals.kt
@@ -24,7 +24,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import cafe.adriel.voyager.core.screen.Screen
import com.getcode.LocalBetaFlags
-import com.getcode.MainRoot
import com.getcode.TopLevelViewModel
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.core.LocalCodeNavigator
@@ -33,14 +32,11 @@ import com.getcode.ui.components.SheetTitle
import com.getcode.ui.components.SheetTitleDefaults
import com.getcode.ui.components.SheetTitleText
import com.getcode.ui.components.keyboardAsState
-import com.getcode.ui.utils.getActivityScopedViewModel
+import com.getcode.navigation.extensions.getActivityScopedViewModel
+import com.getcode.navigation.modal.ModalHeightMetric
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-sealed interface ModalHeightMetric {
- data class Weight(val weight: Float) : ModalHeightMetric
- data object WrapContent : ModalHeightMetric
-}
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -121,7 +117,7 @@ internal fun NamedScreen.ModalContainer(
modifier = Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
) {
- val tlvm = MainRoot.getActivityScopedViewModel()
+ val tlvm = getActivityScopedViewModel()
val state by tlvm.state.collectAsState()
CompositionLocalProvider(
LocalOverscrollConfiguration provides null,
@@ -131,7 +127,4 @@ internal fun NamedScreen.ModalContainer(
}
}
}
-}
-
-internal interface ModalContent
-internal sealed interface ModalRoot : ModalContent
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt b/app/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt
index 18c25c0af..a8a7e0de9 100644
--- a/app/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt
@@ -81,7 +81,7 @@ private fun PhoneCountrySelection(
)
}
Divider(
- color = White05,
+ color = CodeTheme.colors.dividerVariant,
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
diff --git a/app/src/main/java/com/getcode/navigation/screens/Screens.kt b/app/src/main/java/com/getcode/navigation/screens/Screens.kt
index b29ba60a7..3a704229c 100644
--- a/app/src/main/java/com/getcode/navigation/screens/Screens.kt
+++ b/app/src/main/java/com/getcode/navigation/screens/Screens.kt
@@ -5,20 +5,6 @@ import cafe.adriel.voyager.core.screen.Screen
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
-sealed interface NamedScreen {
-
- val name: String?
- @Composable get() = null
-
- val hasName: Boolean
- @Composable get() = !name.isNullOrEmpty()
-}
-
-abstract class AppScreen: Screen {
- var result = MutableStateFlow(null)
-
- fun onResult(obj: T) {
- Timber.d("onResult=$obj")
- result.value = obj
- }
+interface MainScreen {
+ val seed: String?
}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/navigation/transitions/CrossfadeTransition.kt b/app/src/main/java/com/getcode/navigation/transitions/CrossfadeTransition.kt
deleted file mode 100644
index 862d2dd41..000000000
--- a/app/src/main/java/com/getcode/navigation/transitions/CrossfadeTransition.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.getcode.navigation.transitions
-
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import cafe.adriel.voyager.navigator.Navigator
-import cafe.adriel.voyager.transitions.ScreenTransition
-import cafe.adriel.voyager.transitions.ScreenTransitionContent
-
-@Composable
-private fun CrossfadeTransition(
- navigator: Navigator,
- modifier: Modifier = Modifier,
- content: ScreenTransitionContent = { it.Content() }
-) {
- ScreenTransition(
- navigator = navigator,
- modifier = modifier,
- content = content,
- transition = { fadeIn() togetherWith fadeOut() }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt
index f9cd54b18..da1fad6d3 100644
--- a/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt
+++ b/app/src/main/java/com/getcode/notifications/CodePushMessagingService.kt
@@ -18,18 +18,17 @@ import com.getcode.model.notifications.NotificationType
import com.getcode.model.notifications.parse
import com.getcode.network.BalanceController
import com.getcode.network.NotificationCollectionHistoryController
-import com.getcode.network.ChatHistoryController
import com.getcode.network.TipController
import com.getcode.network.repository.AccountRepository
import com.getcode.network.repository.PushRepository
import com.getcode.network.repository.TransactionRepository
+import com.getcode.services.utils.installationId
import com.getcode.ui.components.chat.utils.localizedText
-import com.getcode.util.CurrencyUtils
import com.getcode.util.resources.ResourceHelper
import com.getcode.util.resources.ResourceType
+import com.getcode.utils.CurrencyUtils
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
@@ -76,9 +75,6 @@ class CodePushMessagingService : FirebaseMessagingService(),
@Inject
lateinit var notificationHistory: NotificationCollectionHistoryController
- @Inject
- lateinit var chatHistory: ChatHistoryController
-
@Inject
lateinit var tipController: TipController
@@ -106,14 +102,16 @@ class CodePushMessagingService : FirebaseMessagingService(),
val (type, titleKey, messageContent) = notification
if (type.isNotifiable()) {
val title = titleKey.localizedStringByKey(resources) ?: titleKey
- val body = messageContent.localizedText(title, resources, currencyUtils)
+ val body = messageContent.localizedText(
+ resources = resources,
+ currencyUtils = currencyUtils
+ )
notify(type, title, body)
}
when (type) {
NotificationType.ChatMessage -> {
launch { notificationHistory.fetch() }
- launch { chatHistory.fetch() }
launch { balanceController.fetchBalanceSuspend() }
}
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 25a6002e5..1224add2b 100644
--- a/app/src/main/java/com/getcode/ui/components/AuthCheck.kt
+++ b/app/src/main/java/com/getcode/ui/components/AuthCheck.kt
@@ -10,15 +10,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.screen.Screen
+import com.getcode.AppHomeScreen
import com.getcode.LocalDeeplinks
import com.getcode.R
import com.getcode.manager.BottomBarManager
import com.getcode.manager.SessionManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.AccessKeyLoginScreen
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.LoginGraph
import com.getcode.navigation.screens.LoginScreen
+import com.getcode.navigation.screens.MainScreen
import com.getcode.util.DeeplinkHandler
import com.getcode.util.DeeplinkResult
import com.getcode.ui.utils.getActivity
@@ -138,7 +139,7 @@ fun AuthCheck(
if (authenticated) {
if (!deeplinkRouted) {
trace("Navigating to home")
- onNavigate(listOf(ScanScreen()))
+ onNavigate(listOf(AppHomeScreen()))
}
} else {
tipsEngine?.invalidateAllTips()
@@ -160,7 +161,7 @@ private fun Flow.mapSeedToHome(): Flow =
trace("mapping entropy to home screen")
// send the user to home screen
val entropy = (screens.first() as? LoginScreen)?.seed
- val updatedData = data.copy(stack = listOf(ScanScreen(seed = entropy)))
+ val updatedData = data.copy(stack = listOf(AppHomeScreen(seed = entropy)))
updatedData to auth
} else {
data to auth
@@ -175,7 +176,7 @@ private fun Flow.showLogoutConfirmationIfNeeded(
onCancel: () -> Unit
): Flow = onEach { (type, screens) ->
if (type is DeeplinkHandler.Type.Login) {
- val entropy = (screens.first() as? ScanScreen)?.seed
+ val entropy = (screens.first() as? MainScreen)?.seed
if (entropy != null) {
trace("showing logout confirm")
showLogoutMessage(
diff --git a/app/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt b/app/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt
index a761a1e3d..f87400070 100644
--- a/app/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt
+++ b/app/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt
@@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.getcode.theme.CodeTheme
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.utils.swallowClicks
@Composable
diff --git a/app/src/main/java/com/getcode/ui/components/ModalContainer.kt b/app/src/main/java/com/getcode/ui/components/ModalContainer.kt
index 562451626..8a0bc8270 100644
--- a/app/src/main/java/com/getcode/ui/components/ModalContainer.kt
+++ b/app/src/main/java/com/getcode/ui/components/ModalContainer.kt
@@ -11,7 +11,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import cafe.adriel.voyager.core.stack.StackEvent
import com.getcode.CodeAppState
-import com.getcode.manager.ModalManager
+import com.getcode.services.manager.ModalManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.buildMessageContent
import kotlinx.coroutines.delay
diff --git a/app/src/main/java/com/getcode/ui/components/OtpBox.kt b/app/src/main/java/com/getcode/ui/components/OtpBox.kt
index 02a0624a4..7ed0ba21d 100644
--- a/app/src/main/java/com/getcode/ui/components/OtpBox.kt
+++ b/app/src/main/java/com/getcode/ui/components/OtpBox.kt
@@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.theme.WindowSizeClass
import com.getcode.ui.utils.rememberedClickable
@@ -37,9 +36,9 @@ fun OtpBox(
.rememberedClickable(onClick = onClick)
.border(
border = if (isHighlighted)
- BorderStroke(CodeTheme.dimens.thickBorder, color = BrandLight.copy(alpha = 0.7f))
+ BorderStroke(CodeTheme.dimens.thickBorder, color = CodeTheme.colors.brandLight.copy(alpha = 0.7f))
else
- BorderStroke(CodeTheme.dimens.border, color = BrandLight.copy(alpha = 0.3f)),
+ BorderStroke(CodeTheme.dimens.border, color = CodeTheme.colors.brandLight.copy(alpha = 0.3f)),
shape = CodeTheme.shapes.small
)
.background(Color.White.copy(alpha = 0.1f)),
diff --git a/app/src/main/java/com/getcode/ui/components/TextSection.kt b/app/src/main/java/com/getcode/ui/components/TextSection.kt
index 663ac619c..ef1f82f03 100644
--- a/app/src/main/java/com/getcode/ui/components/TextSection.kt
+++ b/app/src/main/java/com/getcode/ui/components/TextSection.kt
@@ -2,11 +2,8 @@ package com.getcode.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
import com.getcode.theme.CodeTheme
@Composable
diff --git a/app/src/main/java/com/getcode/ui/components/TitleBar.kt b/app/src/main/java/com/getcode/ui/components/TitleBar.kt
deleted file mode 100644
index 37f0d3eca..000000000
--- a/app/src/main/java/com/getcode/ui/components/TitleBar.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.getcode.ui.components
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.ArrowBack
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.getcode.theme.Brand
-import com.getcode.theme.CodeTheme
-import com.getcode.theme.topBarHeight
-import com.getcode.ui.utils.unboundedClickable
-
-
-@Composable
-fun TitleBar(
- modifier: Modifier = Modifier,
- title: String = "",
- backButton: Boolean = false,
- onBackIconClicked: () -> Unit = {}
-) {
- Surface(
- color = Brand,
- elevation = 0.dp
- ) {
- Box(
- modifier = modifier
- .statusBarsPadding()
- .background(Brand)
- .fillMaxWidth()
- .height(topBarHeight),
- ) {
- if (backButton) {
- Icon(
- imageVector = Icons.Outlined.ArrowBack,
- contentDescription = "",
- tint = Color.White,
- modifier = Modifier
- .align(Alignment.CenterStart)
- .padding(start = CodeTheme.dimens.inset)
- .wrapContentWidth()
- .size(24.dp)
- .unboundedClickable { onBackIconClicked() }
- )
- }
- Text(
- text = title,
- color = Color.White,
- style = CodeTheme.typography.screenTitle,
- modifier = Modifier.align(Alignment.Center)
- )
- }
- }
-}
-
-@Preview
-@Composable
-fun Preview_TitleBar(
-
-) {
- CodeTheme {
- TitleBar(backButton = true, title = "Hey")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/AnnouncementMessage.kt b/app/src/main/java/com/getcode/ui/components/chat/AnnouncementMessage.kt
deleted file mode 100644
index c42b86ab4..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/AnnouncementMessage.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.getcode.ui.components.chat
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import com.getcode.theme.BrandDark
-import com.getcode.theme.CodeTheme
-
-@Composable
-fun AnnouncementMessage(
- modifier: Modifier = Modifier,
- text: String,
-) {
- Box(modifier = modifier, contentAlignment = Alignment.Center) {
- Column(
- modifier = Modifier
- .background(
- color = BrandDark,
- shape = MessageNodeDefaults.DefaultShape
- )
- .padding(CodeTheme.dimens.grid.x2),
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- text = text,
- style = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.W500)
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/AnonymousAvatar.kt b/app/src/main/java/com/getcode/ui/components/chat/AnonymousAvatar.kt
deleted file mode 100644
index 7e5794610..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/AnonymousAvatar.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.getcode.ui.components.chat
-
-import android.graphics.Bitmap
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.RectF
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.drawWithCache
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.getcode.network.repository.sha512
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.utils.UUIDPreviewParameterProvider
-import com.getcode.ui.utils.deriveTargetColor
-import com.getcode.ui.utils.toAGColor
-import com.getcode.utils.bytes
-import java.util.UUID
-import kotlin.experimental.and
-import kotlin.math.roundToInt
-
-enum class AnonymousRender {
- EightBit, Gradient
-}
-
-@Composable
-fun AnonymousAvatar(
- memberId: UUID,
- modifier: Modifier = Modifier,
- type: AnonymousRender = AnonymousRender.EightBit
-) {
- AnonymousAvatar(modifier = modifier, data = memberId.bytes, type = type)
-}
-
-@Composable
-fun AnonymousAvatar(
- data: List,
- modifier: Modifier = Modifier,
- type: AnonymousRender = AnonymousRender.EightBit
-) {
- Box(
- modifier = modifier
- .background(Color(0xFFE6F0FA), CircleShape)
- .aspectRatio(1f)
- .clip(CircleShape)
- .fillMaxSize()
- .drawWithCache {
- when (type) {
- AnonymousRender.EightBit -> {
- val avatar = if (size.isEmpty().not()) {
- generateAvatar(data, size)
- } else {
- null
- }
-
- onDrawWithContent {
- if (avatar != null) {
- drawImage(avatar)
- } else {
- drawRect(Color.Transparent)
- }
- }
- }
- AnonymousRender.Gradient -> {
- val hash = runCatching {
- data.toByteArray().sha512()
- }.getOrNull()
-
- val gradient = if (hash != null) {
- val sourceColor = rgbFromHash(hash.copyOfRange(0, 3))
- val derivedColor = deriveTargetColor(sourceColor, 0.3f, 0.5f)
-
- Brush.verticalGradient(
- colors = listOf(sourceColor, derivedColor)
- )
- } else {
- null
- }
-
- onDrawWithContent {
- if (gradient != null) {
- drawRect(brush = gradient)
- } else {
- drawRect(Color.Transparent)
- }
- }
- }
- }
- }
- )
-}
-
-@Preview
-@Composable
-fun Preview_Avatars() {
- CodeTheme {
- val provider = UUIDPreviewParameterProvider(40)
- LazyVerticalGrid(columns = GridCells.Fixed(8)) {
- items(provider.values.toList()) {
- Box(modifier = Modifier.padding(8.dp)) {
- AnonymousAvatar(modifier = Modifier.fillMaxSize(), memberId = it)
- }
- }
- }
- }
-}
-
-private fun generateAvatar(data: List, size: Size): ImageBitmap? {
- val hash = runCatching { data.toByteArray().sha512() }.getOrNull() ?: return null
- val foregroundColor = rgbFromHash(hash.copyOfRange(0, 3))
-
- val bitmap = Bitmap.createBitmap(
- size.width.roundToInt(),
- size.height.roundToInt(),
- Bitmap.Config.ARGB_8888
- )
- val canvas = android.graphics.Canvas(bitmap)
- val paint = Paint()
-
- val length = 10
- val rCount = length
- val cCount = length / 2
- val cellSize = size.width / length.toFloat()
- val inset = cellSize * 0.6f
-
- val bounds = RectF(0f, 0f, size.width, size.height)
- val fullPath = Path().apply { addOval(bounds, Path.Direction.CCW) }
- val maskPath = Path().apply { addOval(bounds.apply { inset(inset, inset) }, Path.Direction.CW) }
-
- val delta = Path().apply {
- op(fullPath, maskPath, Path.Op.DIFFERENCE)
- }
-
- val paths = mutableListOf()
-
- for (r in 0 until rCount) {
- for (c in 0 until cCount) {
- val i = r * cCount + c
- val isEven = (hash[i] and 1) == 0.toByte()
- if (isEven) {
- val leftPath = createPath(r, c, cellSize)
- if (!delta.intersects(leftPath)) {
- paths.add(leftPath)
- }
-
- val rightPath = createPath(r, length - c - 1, cellSize)
- if (!delta.intersects(rightPath)) {
- paths.add(rightPath)
- }
- }
- }
- }
-
- paint.color = foregroundColor.toAGColor()
- paths.forEach { canvas.drawPath(it, paint) }
-
- return bitmap.asImageBitmap()
-}
-
-private fun createPath(row: Int, col: Int, size: Float): Path {
- return Path().apply {
- addRect(col * size, row * size, (col + 1) * size, (row + 1) * size, Path.Direction.CW)
- }
-}
-
-private fun Path.intersects(other: Path): Boolean {
- val result = Path()
- result.op(this, other, Path.Op.INTERSECT)
- return !result.isEmpty
-}
-
-private fun rgbFromHash(hash: ByteArray): Color {
- return Color(hash[0].toInt() and 0xFF, hash[1].toInt() and 0xFF, hash[2].toInt() and 0xFF)
-}
\ No newline at end of file
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
deleted file mode 100644
index fadfe6d84..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/ChatNode.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-package com.getcode.ui.components.chat
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.text.InlineTextContent
-import androidx.compose.foundation.text.appendInlineContent
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.VolumeOff
-import androidx.compose.runtime.Composable
-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.res.painterResource
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.Placeholder
-import androidx.compose.ui.text.PlaceholderVerticalAlign
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.style.TextOverflow
-import com.getcode.R
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.Chat
-import com.getcode.model.chat.MessageContent
-import com.getcode.model.chat.Pointer
-import com.getcode.model.chat.isConversation
-import com.getcode.model.chat.self
-import com.getcode.model.uuid
-import com.getcode.theme.BrandLight
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.Badge
-import com.getcode.ui.components.chat.utils.localized
-import com.getcode.ui.components.chat.utils.localizedText
-import com.getcode.ui.utils.rememberedClickable
-import com.getcode.util.DateUtils
-import com.getcode.util.formatTimeRelatively
-
-object ChatNodeDefaults {
- val UnreadIndicator: Color = Color(0xFF31BB00)
-}
-
-@Composable
-fun ChatNode(
- modifier: Modifier = Modifier,
- chat: Chat,
- showAvatar: Boolean = false,
- onClick: () -> Unit,
-) {
- Row(
- modifier = modifier
- .rememberedClickable { onClick() }
- .padding(
- vertical = CodeTheme.dimens.grid.x3,
- horizontal = CodeTheme.dimens.inset
- ),
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3),
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (showAvatar) {
- val imageModifier = Modifier
- .size(CodeTheme.dimens.staticGrid.x10)
- .clip(CircleShape)
-
- UserAvatar(modifier = imageModifier, data = chat.imageData)
- }
-
- Column(
- modifier = Modifier.weight(1f),
- verticalArrangement = Arrangement.spacedBy(
- CodeTheme.dimens.grid.x1,
- Alignment.CenterVertically
- ),
- ) {
- val hasUnreadMessages by remember(chat.unreadCount) {
- derivedStateOf { chat.unreadCount > 0 }
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- Text(
- text = chat.title.localized,
- maxLines = 1,
- style = CodeTheme.typography.textMedium
- )
- chat.lastMessageMillis?.let {
- val isToday = DateUtils.isToday(it)
- Text(
- text = if (isToday) {
- it.formatTimeRelatively()
- } else {
- DateUtils.getDateRelatively(it)
- },
- style = CodeTheme.typography.textSmall,
- color = if (hasUnreadMessages) ChatNodeDefaults.UnreadIndicator else CodeTheme.colors.brandLight,
- )
- }
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset),
- ) {
- val (preview, inlineContent) = chat.messagePreview
-
- Text(
- modifier = Modifier.weight(1f),
- text = preview,
- inlineContent = inlineContent,
- style = CodeTheme.typography.textMedium,
- color = CodeTheme.colors.brandLight,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- if (chat.isMuted) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.VolumeOff,
- contentDescription = "chat is muted",
- tint = BrandLight
- )
- } else {
- Badge(
- Modifier
- .padding(end = CodeTheme.dimens.grid.x1),
- count = chat.unreadCount,
- color = ChatNodeDefaults.UnreadIndicator
- )
- }
- }
- }
- }
-}
-
-private val Chat.messagePreview: Pair>
- @Composable get() {
- val contents = newestMessage?.contents ?: return AnnotatedString("No content") to emptyMap()
-
- var filtered: List = contents.filterIsInstance()
- if (filtered.isEmpty()) {
- filtered = contents
- }
-
- val selfMember = self
- val pointer =
- selfMember?.pointers.orEmpty().find { it.messageId == newestMessage?.id?.uuid }
-
- // joinToString does expose a Composable scoped lambda
- @Suppress("SimplifiableCallChain")
- val messageBody = filtered.map { it.localizedText }.joinToString(" ")
-
- val textStyle = CodeTheme.typography.textMedium
- return if (pointer != null && pointer !is Pointer.Unknown && isConversation) {
- val string = buildAnnotatedString {
- appendInlineContent("status", "status")
- append(" ")
- append(messageBody)
- }
-
- string to mapOf(
- "status" to InlineTextContent(
- Placeholder(
- textStyle.fontSize,
- textStyle.fontSize,
- PlaceholderVerticalAlign.TextCenter
- )
- ) {
- Image(
- painter = painterResource(
- id = when (pointer) {
- is Pointer.Delivered -> R.drawable.ic_message_status_delivered
- is Pointer.Read -> R.drawable.ic_message_status_read
- is Pointer.Sent -> R.drawable.ic_message_status_sent
- else -> -1
- }
- ),
- modifier = Modifier.fillMaxSize(),
- contentDescription = ""
- )
- }
- )
- } else {
- AnnotatedString(messageBody) to emptyMap()
- }
- }
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/MessageList.kt b/app/src/main/java/com/getcode/ui/components/chat/MessageList.kt
deleted file mode 100644
index d4619e182..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/MessageList.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.getcode.ui.components.chat
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.paging.compose.LazyPagingItems
-import androidx.paging.compose.itemContentType
-import androidx.paging.compose.itemKey
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.MessageContent
-import com.getcode.model.chat.Reference
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.chat.utils.ChatItem
-import com.getcode.util.formatDateRelatively
-
-sealed interface MessageListEvent {
- data class OpenMessageChat(val reference: Reference): MessageListEvent
- data class AdvancePointer(val messageId: ID): MessageListEvent
-}
-@Composable
-fun MessageList(
- modifier: Modifier = Modifier,
- listState: LazyListState = rememberLazyListState(),
- verticalArrangement: Arrangement.Vertical = Arrangement.Top,
- messages: LazyPagingItems,
- dispatch: (MessageListEvent) -> Unit = { },
-) {
-
- LaunchedEffect(messages.itemSnapshotList, listState) {
- snapshotFlow { listState.firstVisibleItemIndex }
- .collect { index ->
- val closetChatMessage = messages.itemSnapshotList.toList().getClosestChat(index)
- if (closetChatMessage != null) {
- val (id, isFromSelf, status) = closetChatMessage
- if (!isFromSelf) {
- dispatch(MessageListEvent.AdvancePointer(id))
- }
- }
- }
- }
-
- LazyColumn(
- modifier = modifier,
- state = listState,
- reverseLayout = true,
-
- contentPadding = PaddingValues(
- horizontal = CodeTheme.dimens.inset,
- vertical = CodeTheme.dimens.inset,
- ),
- verticalArrangement = verticalArrangement,
- ) {
- items(
- count = messages.itemCount,
- key = messages.itemKey { item -> item.key },
- contentType = messages.itemContentType { item ->
- when (item) {
- is ChatItem.Date -> "separators"
- is ChatItem.Message -> "messages"
- }
- }
- ) { index ->
- when (val item = messages[index]) {
- is ChatItem.Date -> DateBubble(
- modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x2),
- date = item.date
- )
- is ChatItem.Message -> {
- // reverse layout so +1 to get previous
- val prev = runCatching { messages[index + 1] }
- .map { it as? ChatItem.Message }
- .map { it?.chatMessageId }
- .getOrNull()
- // reverse layout so -1 to get next
- val next = runCatching { messages[index - 1] }
- .map { it as? ChatItem.Message }
- .map { it?.chatMessageId }
- .getOrNull()
-
- MessageNode(
- modifier = Modifier.fillMaxWidth(),
- contents = item.message,
- status = item.status,
- date = item.date,
- isPreviousSameMessage = prev == item.chatMessageId,
- isNextSameMessage = next == item.chatMessageId,
- )
- }
-
- else -> Unit
- }
- }
- // add last separator
- // this isn't handled by paging separators due to no `beforeItem` to reference against
- // at end of list due to reverseLayout
- if (messages.itemCount > 0) {
- (messages[messages.itemCount - 1] as? ChatItem.Message)?.date?.let { date ->
- item {
- val dateString = remember(date) {
- date.formatDateRelatively()
- }
- DateBubble(
- modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x2),
- date = dateString
- )
- }
- }
- }
- }
-}
-
-
-private fun List.getClosestChat(index: Int): Triple? {
- if (index !in indices) return null
- val item = this[index]
- return when {
- item is ChatItem.Message -> Triple(item.chatMessageId, item.isFromSelf, item.status)
- index > 0 -> getClosestChat(index - 1)
- else -> null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/MessageNode.kt b/app/src/main/java/com/getcode/ui/components/chat/MessageNode.kt
deleted file mode 100644
index 397aadc61..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/MessageNode.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-package com.getcode.ui.components.chat
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.BoxWithConstraintsScope
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.shape.CornerBasedShape
-import androidx.compose.foundation.shape.CornerSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.MessageContent
-import com.getcode.model.chat.Reference
-import com.getcode.theme.BrandDark
-import com.getcode.theme.ChatOutgoing
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.chat.utils.localizedText
-import kotlinx.datetime.Instant
-
-object MessageNodeDefaults {
-
- val DefaultShape: CornerBasedShape
- @Composable get() = CodeTheme.shapes.small
- val PreviousSameShape: CornerBasedShape
- @Composable get() = DefaultShape.copy(topStart = CornerSize(3.dp))
-
- val NextSameShape: CornerBasedShape
- @Composable get() = DefaultShape.copy(bottomStart = CornerSize(3.dp))
-
- val MiddleSameShape: CornerBasedShape
- @Composable get() = DefaultShape.copy(
- topStart = CornerSize(3.dp),
- bottomStart = CornerSize(3.dp)
- )
-}
-
-class MessageNodeScope(
- private val contents: MessageContent,
- private val boxScope: BoxWithConstraintsScope
-) {
- fun Modifier.sizeableWidth() =
- this.widthIn(max = boxScope.maxWidth * 0.85f)
-
- val color: Color
- @Composable get() = if (contents is MessageContent.Exchange && !contents.verb.increasesBalance) {
- CodeTheme.colors.secondary
- } else {
- CodeTheme.colors.brandDark
- }
-
- val isAnnouncement: Boolean
- @Composable get() = remember {
- when (contents) {
- is MessageContent.IdentityRevealed -> true
- is MessageContent.ThankYou -> true
- else -> false
- }
- }
-}
-
-@Composable
-private fun rememberMessageNodeScope(
- contents: MessageContent,
- boxScope: BoxWithConstraintsScope
-): MessageNodeScope {
- return remember(contents, boxScope) {
- MessageNodeScope(contents, boxScope)
- }
-}
-
-@Composable
-fun MessageNode(
- modifier: Modifier = Modifier,
- contents: MessageContent,
- date: Instant,
- status: MessageStatus,
- isPreviousSameMessage: Boolean,
- isNextSameMessage: Boolean,
-) {
- BoxWithConstraints(
- modifier = modifier
- .padding(vertical = CodeTheme.dimens.grid.x1)
- ) {
- val scope = rememberMessageNodeScope(contents = contents, boxScope = this)
-
- with(scope) {
- when (contents) {
- is MessageContent.Exchange -> {
- MessagePayment(
- modifier = Modifier
- .align(if (contents.isFromSelf) Alignment.CenterEnd else Alignment.CenterStart)
- .sizeableWidth()
- .background(
- color = color,
- shape = when {
- isAnnouncement -> MessageNodeDefaults.DefaultShape
- isPreviousSameMessage && isNextSameMessage -> MessageNodeDefaults.MiddleSameShape
- isPreviousSameMessage -> MessageNodeDefaults.PreviousSameShape
- isNextSameMessage -> MessageNodeDefaults.NextSameShape
- else -> MessageNodeDefaults.DefaultShape
- }
- ),
- contents = contents,
- status = status,
- date = date,
- )
- }
-
- is MessageContent.Localized -> {
- MessageText(
- modifier = Modifier.fillMaxWidth(),
- content = contents.localizedText,
- date = date,
- status = status,
- isFromSelf = contents.isFromSelf
- )
- }
-
- is MessageContent.SodiumBox -> {
- EncryptedContent(
- modifier = Modifier
- .align(if (status.isOutgoing()) Alignment.CenterEnd else Alignment.CenterStart)
- .sizeableWidth()
- .background(
- color = color,
- shape = when {
- isAnnouncement -> MessageNodeDefaults.DefaultShape
- isPreviousSameMessage && isNextSameMessage -> MessageNodeDefaults.MiddleSameShape
- isPreviousSameMessage -> MessageNodeDefaults.PreviousSameShape
- isNextSameMessage -> MessageNodeDefaults.NextSameShape
- else -> MessageNodeDefaults.DefaultShape
- }
- )
- .padding(CodeTheme.dimens.grid.x2),
- date = date
- )
- }
-
- is MessageContent.Decrypted -> {
- MessageText(
- modifier = Modifier.fillMaxWidth(),
- content = contents.data,
- date = date,
- status = status,
- isFromSelf = contents.isFromSelf
- )
- }
-
- is MessageContent.IdentityRevealed -> {
- AnnouncementMessage(
- modifier = Modifier.align(Alignment.Center),
- text = contents.localizedText
- )
- }
- is MessageContent.RawText -> {
- MessageText(
- modifier = Modifier.fillMaxWidth(),
- content = contents.value,
- date = date,
- status = status,
- isFromSelf = contents.isFromSelf
- )
- }
- is MessageContent.ThankYou -> {
- AnnouncementMessage(
- modifier = Modifier.align(Alignment.Center),
- text = contents.localizedText
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/MessageTextContent.kt b/app/src/main/java/com/getcode/ui/components/chat/MessageTextContent.kt
deleted file mode 100644
index a182b39bb..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/MessageTextContent.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package com.getcode.ui.components.chat
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.rememberTextMeasurer
-import com.getcode.model.MessageStatus
-import com.getcode.theme.CodeTheme
-import com.getcode.util.formatDateRelatively
-import kotlinx.datetime.Instant
-
-@Composable
-fun MessageNodeScope.MessageText(
- modifier: Modifier = Modifier,
- content: String,
- isFromSelf: Boolean,
- date: Instant,
- status: MessageStatus = MessageStatus.Unknown,
-) {
- val alignment = if (isFromSelf) Alignment.CenterEnd else Alignment.CenterStart
-
- BoxWithConstraints(modifier = modifier.fillMaxWidth(), contentAlignment = alignment) {
- BoxWithConstraints(
- modifier = Modifier
- .sizeableWidth()
- .background(
- color = color,
- shape = MessageNodeDefaults.DefaultShape
- )
- .padding(CodeTheme.dimens.grid.x2)
- ) contents@{
- val maxWidthPx = with (LocalDensity.current) { maxWidth.roundToPx() }
- Column(
- modifier = Modifier
- .background(color)
- // add top padding to accommodate ascents
- .padding(top = CodeTheme.dimens.grid.x1),
- ) {
- MessageContent(
- maxWidth = maxWidthPx,
- message = content, date = date, status = status, isFromSelf = isFromSelf)
- }
- }
- }
-}
-
-@Composable
-private fun rememberAlignmentRule(
- contentTextStyle: TextStyle,
- maxWidth: Int,
- message: String,
- date: Instant
-): State {
- val density = LocalDensity.current
- val dateTextStyle = DateWithStatusDefaults.DateTextStyle
- val iconSizePx = with (density) { DateWithStatusDefaults.IconWidth.roundToPx() }
- val spacingPx = with (density) { DateWithStatusDefaults.Spacing.roundToPx() }
- val contentPaddingPx = with (density) { CodeTheme.dimens.grid.x2.roundToPx() }
-
- return remember(maxWidth, message, date) {
- mutableStateOf(null)
- }.apply {
- val textMeasurer = rememberTextMeasurer()
- val dateStatusWidth = remember(message, date) {
- val result = textMeasurer.measure(
- text = date.formatDateRelatively(),
- style = dateTextStyle,
- maxLines = 1
- )
- result.size.width + spacingPx + iconSizePx
- }
-
- val bufferSize by remember(dateStatusWidth) {
- derivedStateOf {
- dateStatusWidth + spacingPx + contentPaddingPx * 2
- }
- }
-
- if (value == null) {
- Text(
- modifier = Modifier.drawWithContent { },
- text = message,
- style = contentTextStyle,
- onTextLayout = { textLayoutResult ->
- val lastLineNum = textLayoutResult.lineCount - 1
- val lineStart = textLayoutResult.getLineStart(lastLineNum)
- val lineEnd =
- textLayoutResult.getLineEnd(lastLineNum, visibleEnd = true)
- val lineContent = message.substring(lineStart, lineEnd)
-
- val lineContentWidth =
- textMeasurer.measure(lineContent, contentTextStyle).size.width
-
- value = when {
- lineContentWidth + bufferSize > maxWidth -> AlignmentRule.Column
- textLayoutResult.lineCount == 1 -> AlignmentRule.SingleLineEnd
- else -> AlignmentRule.ParagraphLastLine
- }
- }
- )
- }
- }
-}
-
-@Composable
-private fun MessageContent(maxWidth: Int, message: String, date: Instant, status: MessageStatus, isFromSelf: Boolean) {
- val contentStyle = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.W500)
- val alignmentRule by rememberAlignmentRule(
- contentTextStyle = contentStyle,
- maxWidth = maxWidth,
- message = message,
- date = date,
- )
-
- when (alignmentRule) {
- AlignmentRule.Column -> {
- Column(verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2)) {
- Text(
- text = message,
- style = contentStyle,
- )
- DateWithStatus(
- modifier = Modifier
- .align(Alignment.End),
- date = date,
- status = status,
- isFromSelf = isFromSelf
- )
- }
- }
- AlignmentRule.ParagraphLastLine -> {
- Column(verticalArrangement = Arrangement.spacedBy(-(CodeTheme.dimens.grid.x2))) {
- Text(
- text = message,
- style = contentStyle,
- )
- DateWithStatus(
- modifier = Modifier
- .align(Alignment.End),
- date = date,
- status = status,
- isFromSelf = isFromSelf
- )
- }
- }
- AlignmentRule.SingleLineEnd -> {
- Row(horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1)) {
- Text(
- text = message,
- style = contentStyle,
- )
- DateWithStatus(
- modifier = Modifier
- .padding(top = CodeTheme.dimens.grid.x1),
- date = date,
- status = status,
- isFromSelf = isFromSelf
- )
- }
- }
- else -> Unit
- }
-}
-
-sealed interface AlignmentRule {
- data object ParagraphLastLine: AlignmentRule
- data object Column: AlignmentRule
- data object SingleLineEnd: AlignmentRule
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/utils/ChatItem.kt b/app/src/main/java/com/getcode/ui/components/chat/utils/ChatItem.kt
deleted file mode 100644
index b3e6e00cf..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/utils/ChatItem.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.getcode.ui.components.chat.utils
-
-import com.getcode.model.ID
-import com.getcode.model.MessageStatus
-import com.getcode.model.chat.ChatMessage
-import com.getcode.model.chat.MessageContent
-import kotlinx.datetime.Instant
-import java.util.UUID
-
-data class ChatMessageIndice(
- val message: ChatMessage,
- val messageContent: MessageContent,
-)
-
-sealed class ChatItem(open val key: Any) {
- data class Message(
- val id: String = UUID.randomUUID().toString(),
- val chatMessageId: ID,
- val message: MessageContent,
- val date: Instant,
- val status: MessageStatus,
- val isFromSelf: Boolean,
- override val key: Any = id
- ) : ChatItem(key)
-
- data class Date(val date: String) : ChatItem(date)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/components/chat/utils/ConversationItem.kt b/app/src/main/java/com/getcode/ui/components/chat/utils/ConversationItem.kt
deleted file mode 100644
index 34e7df785..000000000
--- a/app/src/main/java/com/getcode/ui/components/chat/utils/ConversationItem.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.getcode.ui.components.chat.utils
-
-import com.getcode.model.ConversationMessage
-import com.getcode.model.ConversationMessageContent
-import com.getcode.model.chat.MessageContent
-
-data class ConversationMessageIndice(
- val message: ConversationMessage,
- val messageContent: MessageContent,
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/modals/Confirmations.kt b/app/src/main/java/com/getcode/ui/modals/Confirmations.kt
index 120bed617..5c551d8a9 100644
--- a/app/src/main/java/com/getcode/ui/modals/Confirmations.kt
+++ b/app/src/main/java/com/getcode/ui/modals/Confirmations.kt
@@ -16,7 +16,6 @@ import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.zIndex
import cafe.adriel.voyager.navigator.currentOrThrow
import com.getcode.LocalSession
import com.getcode.R
@@ -43,7 +42,7 @@ fun ConfirmationModals(
val showScrim by remember(billState) {
derivedStateOf {
val loginConfirmation = billState.loginConfirmation
- val paymentConfirmation = billState.paymentConfirmation
+ val paymentConfirmation = billState.privatePaymentConfirmation
val socialPaymentConfirmation = billState.socialUserPaymentConfirmation
listOf(loginConfirmation, paymentConfirmation, socialPaymentConfirmation).any {
@@ -68,7 +67,7 @@ fun ConfirmationModals(
// Payment Confirmation container
AnimatedContent(
modifier = Modifier.align(BottomCenter),
- targetState = sessionState.billState.paymentConfirmation?.payload, // payload is constant across state changes
+ targetState = sessionState.billState.privatePaymentConfirmation?.payload, // payload is constant across state changes
transitionSpec = AnimationUtils.modalAnimationSpec(),
label = "payment confirmation",
) {
@@ -77,7 +76,7 @@ fun ConfirmationModals(
contentAlignment = BottomCenter
) {
PaymentConfirmation(
- confirmation = sessionState.billState.paymentConfirmation,
+ confirmation = sessionState.billState.privatePaymentConfirmation,
balance = sessionState.balance,
onAddKin = {
session.rejectPayment()
diff --git a/app/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt b/app/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt
index 6a32aedf4..000b7318e 100644
--- a/app/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt
+++ b/app/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt
@@ -15,8 +15,8 @@ import com.getcode.R
import com.getcode.models.ConfirmationState
import com.getcode.models.LoginConfirmation
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.Modal
import com.getcode.ui.components.SlideToConfirm
import com.getcode.ui.components.SlideToConfirmDefaults
diff --git a/app/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt b/app/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt
index 33277a57e..80be81329 100644
--- a/app/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt
+++ b/app/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt
@@ -25,30 +25,32 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.getcode.R
-import com.getcode.model.CodePayload
+import com.getcode.services.model.CodePayload
import com.getcode.model.CurrencyCode
import com.getcode.model.Fiat
import com.getcode.model.Kin.Companion.fromFiat
import com.getcode.model.KinAmount
-import com.getcode.model.Kind
+import com.getcode.services.model.Kind
import com.getcode.model.Rate
-import com.getcode.models.PaymentConfirmation
+import com.getcode.model.fromFiatAmount
+import com.getcode.models.PrivatePaymentConfirmation
import com.getcode.models.ConfirmationState
import com.getcode.theme.CodeTheme
+import com.getcode.theme.DesignSystem
import com.getcode.theme.bolded
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.Modal
+import com.getcode.ui.components.PriceWithFlag
import com.getcode.ui.components.SlideToConfirm
import com.getcode.ui.components.SlideToConfirmDefaults
-import com.getcode.view.main.scanner.components.PriceWithFlag
import kotlinx.coroutines.delay
@Composable
internal fun PaymentConfirmation(
modifier: Modifier = Modifier,
balance: KinAmount?,
- confirmation: PaymentConfirmation?,
+ confirmation: PrivatePaymentConfirmation?,
onAddKin: () -> Unit = { },
onSend: () -> Unit,
onCancel: () -> Unit,
@@ -106,7 +108,7 @@ private val payload = CodePayload(
).map { it.toByte() }
)
-private fun confirmationWithState(state: ConfirmationState) = PaymentConfirmation(
+private fun confirmationWithState(state: ConfirmationState) = PrivatePaymentConfirmation(
state = state,
payload = payload,
requestedAmount = KinAmount.fromFiatAmount(
@@ -124,7 +126,7 @@ private fun confirmationWithState(state: ConfirmationState) = PaymentConfirmatio
@Preview(showBackground = true)
@Composable
fun Preview_PaymentConfirmModal_Awaiting() {
- CodeTheme {
+ DesignSystem {
Box(
modifier = Modifier
.fillMaxSize()
@@ -145,7 +147,7 @@ fun Preview_PaymentConfirmModal_Awaiting() {
@Preview(showBackground = true)
@Composable
fun Preview_PaymentConfirmModal_Sending() {
- CodeTheme {
+ DesignSystem {
Box(
modifier = Modifier
.fillMaxSize()
@@ -166,7 +168,7 @@ fun Preview_PaymentConfirmModal_Sending() {
@Preview(showBackground = true)
@Composable
fun Preview_PaymentConfirmModal_Sent() {
- CodeTheme {
+ DesignSystem {
Box(
modifier = Modifier
.fillMaxSize()
@@ -187,15 +189,15 @@ fun Preview_PaymentConfirmModal_Sent() {
@Preview(showBackground = true)
@Composable
fun Preview_PaymentConfirmModal_Interactive() {
- CodeTheme {
+ DesignSystem {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
var confirmation by remember {
- mutableStateOf(
- PaymentConfirmation(
+ mutableStateOf(
+ PrivatePaymentConfirmation(
state = ConfirmationState.AwaitingConfirmation,
payload = payload,
requestedAmount = KinAmount.fromFiatAmount(
@@ -274,7 +276,6 @@ private fun PaymentConfirmationContent(
}
SlideToConfirm(
isLoading = isSending,
- trackColor = SlideToConfirmDefaults.BlueTrackColor,
isSuccess = state is ConfirmationState.Sent,
onConfirm = { onApproved() },
)
diff --git a/app/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt b/app/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt
index f7dba4b29..9e93d7e9f 100644
--- a/app/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt
+++ b/app/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt
@@ -13,12 +13,12 @@ import com.getcode.models.Bill
import com.getcode.theme.Brand
import com.getcode.theme.CodeTheme
import com.getcode.theme.White
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.Modal
-import com.getcode.util.flagResId
-import com.getcode.util.formatted
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.utils.flagResId
+import com.getcode.extensions.formatted
+import com.getcode.ui.components.text.AmountArea
@Composable
internal fun ReceivedKinConfirmation(
diff --git a/app/src/main/java/com/getcode/ui/theme/CodeTheme.kt b/app/src/main/java/com/getcode/ui/theme/CodeTheme.kt
new file mode 100644
index 000000000..09a3a2094
--- /dev/null
+++ b/app/src/main/java/com/getcode/ui/theme/CodeTheme.kt
@@ -0,0 +1,9 @@
+package com.getcode.ui.theme
+
+import androidx.compose.runtime.Composable
+import com.getcode.theme.DesignSystem
+
+@Composable
+fun CodeTheme(content: @Composable () -> Unit) {
+ DesignSystem(content = content)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/utils/LazyList.kt b/app/src/main/java/com/getcode/ui/utils/LazyList.kt
deleted file mode 100644
index 5a8b1168a..000000000
--- a/app/src/main/java/com/getcode/ui/utils/LazyList.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.getcode.ui.utils
-
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-
-
-fun LazyListScope.animatedItem(
- key: Any? = null,
- contentType: Any? = null,
- visible: Boolean,
- enter: EnterTransition? = null,
- exit: ExitTransition? = null,
- content: @Composable LazyItemScope.() -> Unit
-) {
- item(key = key, contentType = contentType) {
- if (enter != null && exit != null) {
- AnimatedVisibility(
- visible = visible,
- enter = enter,
- exit = exit,
- ) {
- content()
- }
- } else {
- AnimatedVisibility(
- visible = visible,
- ) {
- content()
- }
- }
- }
-}
-
-fun LazyListState.isScrolledToTheEnd() =
- layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
-
-fun LazyListState.isScrolledToTheBeginning() =
- (layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0) == 0
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/ui/utils/PreviewParameters.kt b/app/src/main/java/com/getcode/ui/utils/PreviewParameters.kt
deleted file mode 100644
index fb67f6106..000000000
--- a/app/src/main/java/com/getcode/ui/utils/PreviewParameters.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.getcode.ui.utils
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import java.util.UUID
-
-class UUIDPreviewParameterProvider(count: Int = 40) : PreviewParameterProvider {
- override val values: Sequence = generateSequence { UUID.randomUUID() }.take(count)
-}
\ 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 0d1c2b1d9..46f70aecb 100644
--- a/app/src/main/java/com/getcode/util/AccountUtils.kt
+++ b/app/src/main/java/com/getcode/util/AccountUtils.kt
@@ -86,7 +86,10 @@ object AccountUtils {
val am: AccountManager = AccountManager.get(context)
val account = am.getAccountsByType(ACCOUNT_TYPE).firstOrNull()
if (account == null) {
- trace("no associated account found", type = TraceType.Error)
+ trace(
+ "no associated account found",
+ type = TraceType.Error
+ )
cont.resume(null)
return@suspendCancellableCoroutine
}
@@ -103,7 +106,11 @@ object AccountUtils {
cont.resume(authToken.orEmpty() to account)
} catch (e: AuthenticatorException) {
- trace(message = "failed to read account", error = e, type = TraceType.Error)
+ trace(
+ message = "failed to read account",
+ error = e,
+ type = TraceType.Error
+ )
cont.resume(null)
}
}, handler
diff --git a/app/src/main/java/com/getcode/util/AndroidLocale.kt b/app/src/main/java/com/getcode/util/AndroidLocale.kt
index 74fe0f32e..357737ebd 100644
--- a/app/src/main/java/com/getcode/util/AndroidLocale.kt
+++ b/app/src/main/java/com/getcode/util/AndroidLocale.kt
@@ -1,16 +1,15 @@
package com.getcode.util
import android.content.Context
-import com.getcode.App
import com.getcode.model.Currency
import com.getcode.util.locale.LocaleHelper
-import com.getcode.utils.LocaleUtils
+import com.getcode.util.locale.LocaleUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class AndroidLocale @Inject constructor(
@ApplicationContext private val context: Context,
- private val currencyUtils: CurrencyUtils,
+ private val currencyUtils: com.getcode.utils.CurrencyUtils,
): LocaleHelper {
override fun getDefaultCurrencyName(): String {
return LocaleUtils.getDefaultCurrency(context)
diff --git a/app/src/main/java/com/getcode/util/AuthenticatorService.kt b/app/src/main/java/com/getcode/util/AuthenticatorService.kt
index 902394748..b53df149b 100644
--- a/app/src/main/java/com/getcode/util/AuthenticatorService.kt
+++ b/app/src/main/java/com/getcode/util/AuthenticatorService.kt
@@ -5,7 +5,6 @@ import android.app.Service
import android.content.Intent
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
@AndroidEntryPoint
diff --git a/app/src/main/java/com/getcode/util/ChromeTabsUtils.kt b/app/src/main/java/com/getcode/util/ChromeTabsUtils.kt
index 11e0b777e..4d68cee16 100644
--- a/app/src/main/java/com/getcode/util/ChromeTabsUtils.kt
+++ b/app/src/main/java/com/getcode/util/ChromeTabsUtils.kt
@@ -2,7 +2,6 @@ package com.getcode.util
import android.content.ComponentName
import android.content.Context
-import android.graphics.BitmapFactory
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
diff --git a/app/src/main/java/com/getcode/util/Context.kt b/app/src/main/java/com/getcode/util/Context.kt
index 66068f231..84c66273e 100644
--- a/app/src/main/java/com/getcode/util/Context.kt
+++ b/app/src/main/java/com/getcode/util/Context.kt
@@ -2,18 +2,12 @@ package com.getcode.util
import android.content.Context
import android.graphics.Bitmap
-import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
-import android.provider.MediaStore
import android.provider.MediaStore.Images.Media
import androidx.core.content.ContextCompat
import com.getcode.R
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.io.InputStream
-import kotlin.math.floor
fun Context.launchAppSettings() {
@@ -40,6 +34,6 @@ fun Context.uriToBitmap(uri: Uri): Bitmap? {
decoder.isMutableRequired = true
}
} else {
- MediaStore.Images.Media.getBitmap(contentResolver, uri)
+ Media.getBitmap(contentResolver, uri)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt
index f1a30b411..9b2e94eec 100644
--- a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt
+++ b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt
@@ -7,15 +7,15 @@ import android.os.Parcelable
import androidx.core.net.toUri
import cafe.adriel.voyager.core.screen.Screen
import com.getcode.R
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
import com.getcode.models.DeepLinkRequest
import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.LoginScreen
import com.getcode.network.repository.BetaFlagsRepository
-import com.getcode.network.repository.urlDecode
+import com.getcode.services.utils.base64EncodedData
import com.getcode.utils.TraceType
-import com.getcode.utils.base64EncodedData
import com.getcode.utils.trace
+import com.getcode.utils.urlDecode
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
@@ -49,13 +49,15 @@ class DeeplinkHandler @Inject constructor(
set(value) {
intent.value = value
field = value
- trace("debounced intent data=${value?.data}", type = TraceType.Silent)
+ trace(
+ "debounced intent data=${value?.data}",
+ type = TraceType.Silent
+ )
}
val intent = MutableStateFlow(debounceIntent)
suspend fun handle(intent: Intent? = debounceIntent): DeeplinkResult? {
- println(intent)
var uri = when {
intent?.data != null -> intent.data
intent?.getStringExtra(Intent.EXTRA_TEXT) != null -> {
diff --git a/app/src/main/java/com/getcode/util/IntentUtils.kt b/app/src/main/java/com/getcode/util/IntentUtils.kt
index ada046265..e5b02111c 100644
--- a/app/src/main/java/com/getcode/util/IntentUtils.kt
+++ b/app/src/main/java/com/getcode/util/IntentUtils.kt
@@ -1,11 +1,10 @@
package com.getcode.util
-import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import com.getcode.BuildConfig
-import com.getcode.utils.makeE164
+import com.getcode.services.utils.makeE164
object IntentUtils {
diff --git a/app/src/main/java/com/getcode/util/Linkify.kt b/app/src/main/java/com/getcode/util/Linkify.kt
index 3eb42501f..525576eba 100644
--- a/app/src/main/java/com/getcode/util/Linkify.kt
+++ b/app/src/main/java/com/getcode/util/Linkify.kt
@@ -1,6 +1,6 @@
package com.getcode.util
-import com.getcode.network.repository.urlEncode
+import com.getcode.utils.urlEncode
object Linkify {
fun cashLink(entropy: String): String = "https://cash.getcode.com/c/#/e=${entropy}"
diff --git a/app/src/main/java/com/getcode/util/PhoneUtils.kt b/app/src/main/java/com/getcode/util/PhoneUtils.kt
index 43b3aa67b..f7e02de7b 100644
--- a/app/src/main/java/com/getcode/util/PhoneUtils.kt
+++ b/app/src/main/java/com/getcode/util/PhoneUtils.kt
@@ -2,13 +2,10 @@ package com.getcode.util
import android.content.Context
import android.telephony.PhoneNumberUtils
-import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.staticCompositionLocalOf
-import com.getcode.App
+import com.getcode.utils.CurrencyUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import io.michaelrocks.libphonenumber.android.NumberParseException
import io.michaelrocks.libphonenumber.android.PhoneNumberUtil
-import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/app/src/main/java/com/getcode/view/MainActivity.kt b/app/src/main/java/com/getcode/view/MainActivity.kt
index d35c87e2a..ef39fb145 100644
--- a/app/src/main/java/com/getcode/view/MainActivity.kt
+++ b/app/src/main/java/com/getcode/view/MainActivity.kt
@@ -1,22 +1,22 @@
package com.getcode.view
+import android.app.Activity
import android.content.Intent
import android.os.Bundle
+import android.os.Process.killProcess
+import android.os.Process.myPid
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.viewModels
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
+import com.getcode.BuildConfig
import com.getcode.CodeApp
import com.getcode.LocalAnalytics
-import com.getcode.LocalCurrencyUtils
import com.getcode.LocalDeeplinks
import com.getcode.LocalDownloadQrCode
-import com.getcode.LocalExchange
-import com.getcode.LocalNetworkObserver
import com.getcode.LocalPhoneFormatter
import com.getcode.LocalSession
import com.getcode.R
@@ -25,25 +25,34 @@ 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.network.exchange.LocalExchange
import com.getcode.ui.tips.DefinedTips
-import com.getcode.ui.utils.handleUncaughtException
-import com.getcode.util.CurrencyUtils
import com.getcode.util.DeeplinkHandler
import com.getcode.util.PhoneUtils
-import com.getcode.util.rememberQrBitmapPainter
+import com.getcode.libs.qr.rememberQrBitmapPainter
+import com.getcode.util.resources.LocalResources
+import com.getcode.util.resources.ResourceHelper
import com.getcode.util.vibration.LocalVibrator
import com.getcode.util.vibration.Vibrator
+import com.getcode.utils.CurrencyUtils
+import com.getcode.utils.LocalCurrencyUtils
+import com.getcode.utils.network.LocalNetworkObserver
import com.getcode.utils.network.NetworkConnectivityListener
import com.getcode.utils.trace
+import com.google.firebase.crashlytics.FirebaseCrashlytics
import dagger.hilt.android.AndroidEntryPoint
import dev.bmcreations.tipkit.engines.LocalTipsEngine
import dev.bmcreations.tipkit.engines.TipsEngine
import timber.log.Timber
import javax.inject.Inject
+import kotlin.system.exitProcess
@AndroidEntryPoint
class MainActivity : FragmentActivity() {
+ @Inject
+ lateinit var resources: ResourceHelper
+
@Inject
lateinit var client: Client
@@ -85,17 +94,15 @@ class MainActivity : FragmentActivity() {
* Invoking the navigation controller here will cause the intent to be fired
* again we want to debounce this once when the activity is started with an intent.
*/
- override fun onNewIntent(intent: Intent?) {
+ override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- if (intent != null) {
- val cachedIntent = deeplinkHandler.debounceIntent
- if (cachedIntent != null && cachedIntent.data == intent.data) {
- Timber.d("Debouncing Intent " + intent.data)
- deeplinkHandler.debounceIntent = null
- return
- }
- deeplinkHandler.debounceIntent = intent
+ val cachedIntent = deeplinkHandler.debounceIntent
+ if (cachedIntent != null && cachedIntent.data == intent.data) {
+ Timber.d("Debouncing Intent " + intent.data)
+ deeplinkHandler.debounceIntent = null
+ return
}
+ deeplinkHandler.debounceIntent = intent
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -112,7 +119,7 @@ class MainActivity : FragmentActivity() {
BoxWithConstraints {
trace("set content")
- val downloadQr = rememberQrBitmapPainter(
+ val downloadQr = com.getcode.libs.qr.rememberQrBitmapPainter(
content = stringResource(
R.string.app_download_link,
stringResource(id = R.string.app_download_link_qr_ref)
@@ -131,7 +138,8 @@ class MainActivity : FragmentActivity() {
LocalCurrencyUtils provides currencyUtils,
LocalExchange provides exchange,
LocalDownloadQrCode provides downloadQr,
- LocalTipsEngine provides tipEngine
+ LocalTipsEngine provides tipEngine,
+ LocalResources provides resources
) {
CodeApp(tipEngine)
}
@@ -156,3 +164,21 @@ class MainActivity : FragmentActivity() {
}
}
+private fun Activity.handleUncaughtException() {
+ val crashedKey = "isCrashed"
+ if (intent.getBooleanExtra(crashedKey, false)) return
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ if (BuildConfig.DEBUG) throw throwable
+
+ FirebaseCrashlytics.getInstance().recordException(throwable)
+
+ val intent = Intent(this, MainActivity::class.java).apply {
+ putExtra(crashedKey, true)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ finish()
+ killProcess(myPid())
+ exitProcess(2)
+ }
+}
diff --git a/app/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt b/app/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt
index 940efd402..987e429b8 100644
--- a/app/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt
+++ b/app/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt
@@ -31,10 +31,10 @@ import androidx.compose.ui.text.style.TextAlign
import com.getcode.LocalDownloadQrCode
import com.getcode.R
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
+import com.getcode.ui.theme.ButtonState
import com.getcode.ui.components.Cloudy
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.components.Row
import com.getcode.ui.components.SelectionContainer
import com.getcode.ui.components.rememberSelectionState
diff --git a/app/src/main/java/com/getcode/view/login/AccessKey.kt b/app/src/main/java/com/getcode/view/login/AccessKey.kt
index 203f70ecc..f6b85cd2f 100644
--- a/app/src/main/java/com/getcode/view/login/AccessKey.kt
+++ b/app/src/main/java/com/getcode/view/login/AccessKey.kt
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
@@ -44,7 +43,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.hilt.navigation.compose.hiltViewModel
-import com.getcode.LocalTopBarPadding
import com.getcode.R
import com.getcode.manager.BottomBarManager
import com.getcode.manager.TopBarManager
@@ -52,19 +50,19 @@ import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.LoginArgs
import com.getcode.theme.CodeTheme
import com.getcode.theme.White
-import com.getcode.ui.components.ButtonState
+import com.getcode.ui.LocalTopBarPadding
+import com.getcode.ui.theme.ButtonState
import com.getcode.ui.components.Cloudy
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.PermissionResult
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.SelectionContainer
-import com.getcode.ui.components.getPermissionLauncher
-import com.getcode.ui.components.rememberPermissionChecker
import com.getcode.ui.components.rememberSelectionState
import com.getcode.ui.utils.addIf
import com.getcode.ui.utils.measured
import com.getcode.util.launchAppSettings
+import com.getcode.util.permissions.PermissionResult
+import com.getcode.util.permissions.getPermissionLauncher
+import com.getcode.util.permissions.rememberPermissionHandler
-@OptIn(ExperimentalComposeUiApi::class)
@Preview
@Composable
fun AccessKey(
@@ -98,7 +96,7 @@ fun AccessKey(
}
val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult)
- val permissionChecker = rememberPermissionChecker()
+ val permissionChecker = rememberPermissionHandler()
if (isExportSeedRequested && isStoragePermissionGranted) {
viewModel.onSubmit(navigator, true)
diff --git a/app/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt b/app/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt
index fbe352146..fe45abcca 100644
--- a/app/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt
@@ -3,20 +3,21 @@ package com.getcode.view.login
import android.Manifest
import android.annotation.SuppressLint
import android.os.Build
+import com.getcode.AppHomeScreen
import com.getcode.analytics.Action
import com.getcode.analytics.ActionSource
import com.getcode.analytics.AnalyticsService
+import com.getcode.libs.qr.QRCodeGenerator
import com.getcode.manager.AuthManager
-import com.getcode.manager.MnemonicManager
+import com.getcode.services.manager.MnemonicManager
import com.getcode.media.MediaScanner
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.CodeLoginPermission
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.LoginScreen
import com.getcode.navigation.screens.PermissionRequestScreen
-import com.getcode.network.repository.getPublicKeyBase58
import com.getcode.util.permissions.PermissionChecker
import com.getcode.util.resources.ResourceHelper
+import com.getcode.utils.getPublicKeyBase58
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
@@ -33,7 +34,8 @@ class AccessKeyViewModel @Inject constructor(
private val mnemonicManager: MnemonicManager,
resources: ResourceHelper,
mediaScanner: MediaScanner,
-) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner) {
+ qrCodeGenerator: QRCodeGenerator,
+) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, qrCodeGenerator) {
@SuppressLint("CheckResult")
fun onSubmit(navigator: CodeNavigator, isSaveImage: Boolean, isDeepLink: Boolean = false) {
val entropyB64 = uiFlow.value.entropyB64 ?: return
@@ -84,7 +86,7 @@ class AccessKeyViewModel @Inject constructor(
} else {
if (Build.VERSION.SDK_INT < 33) {
analytics.action(Action.CompletedOnboarding)
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
} else {
val notificationsPermissionDenied = permissions.isDenied(
Manifest.permission.POST_NOTIFICATIONS
@@ -94,7 +96,7 @@ class AccessKeyViewModel @Inject constructor(
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, true))
} else {
analytics.action(Action.CompletedOnboarding)
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
}
}
}
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 5f3b36fef..7810af0ef 100644
--- a/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt
@@ -9,33 +9,29 @@ import androidx.core.graphics.applyCanvas
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.viewModelScope
import com.getcode.R
-import com.getcode.manager.MnemonicManager
+import com.getcode.libs.qr.QRCodeGenerator
+import com.getcode.services.manager.MnemonicManager
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.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
import com.getcode.theme.White
import com.getcode.ui.utils.toAGColor
-import com.getcode.util.generateQrCode
import com.getcode.util.resources.ResourceHelper
import com.getcode.util.save
-import com.getcode.utils.ErrorUtils
-import com.getcode.vendor.Base58
+import com.getcode.utils.decodeBase64
import com.getcode.view.BaseViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.launch
+import org.kin.sdk.base.tools.Base58
import timber.log.Timber
-import java.io.File
-import java.io.FileOutputStream
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
@@ -58,6 +54,7 @@ abstract class BaseAccessKeyViewModel(
private val resources: ResourceHelper,
private val mnemonicManager: MnemonicManager,
private val mediaScanner: MediaScanner,
+ private val qrCodeGenerator: QRCodeGenerator,
) : BaseViewModel(resources) {
val uiFlow = MutableStateFlow(AccessKeyUiModel())
@@ -241,7 +238,7 @@ abstract class BaseAccessKeyViewModel(
val base58 = Base58.encode(entropyB64.decodeBase64())
val url = "${resources.getString(R.string.root_url_app)}/login?data=$base58"
- return generateQrCode(url, qrCodeSize)
+ return qrCodeGenerator.generate(url, qrCodeSize)
}
private fun drawText(
diff --git a/app/src/main/java/com/getcode/view/login/CameraPermission.kt b/app/src/main/java/com/getcode/view/login/CameraPermission.kt
index 8ec87ae2c..e181be948 100644
--- a/app/src/main/java/com/getcode/view/login/CameraPermission.kt
+++ b/app/src/main/java/com/getcode/view/login/CameraPermission.kt
@@ -9,17 +9,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.constraintlayout.compose.ConstraintLayout
+import com.getcode.AppHomeScreen
import com.getcode.LocalAnalytics
import com.getcode.R
import com.getcode.analytics.Action
import com.getcode.navigation.screens.CodeLoginPermission
import com.getcode.navigation.core.CodeNavigator
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.PermissionRequestScreen
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.util.permissions.cameraPermissionCheck
@Composable
fun CameraPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) {
@@ -33,14 +34,15 @@ fun CameraPermission(navigator: CodeNavigator = LocalCodeNavigator.current, from
if (fromOnboarding) {
analytics.action(Action.CompletedOnboarding)
}
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
} else {
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, fromOnboarding))
}
}
}
- val notificationPermissionCheck = notificationPermissionCheck { onNotificationResult(it) }
+ val notificationPermissionCheck =
+ com.getcode.util.permissions.notificationPermissionCheck { onNotificationResult(it) }
val onCameraResult: (Boolean) -> Unit = { isGranted ->
if (isGranted) {
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 c9e21c3b4..8f25f4b29 100644
--- a/app/src/main/java/com/getcode/view/login/LoginHome.kt
+++ b/app/src/main/java/com/getcode/view/login/LoginHome.kt
@@ -23,18 +23,15 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import com.getcode.LocalAnalytics
import com.getcode.R
import com.getcode.theme.Brand
import com.getcode.theme.CodeTheme
-import com.getcode.util.ChromeTabsUtils
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.ImageWithBackground
-
-
+import com.getcode.util.ChromeTabsUtils
@Composable
fun LoginHome(
@@ -109,7 +106,7 @@ fun LoginHome(
append(" ")
append(stringResource(R.string.login_description_agreeToOur))
append(" ")
- pushStringAnnotation(tag = "tos", annotation = "https://app.getcode.com/tos")
+ pushStringAnnotation(tag = "tos", annotation = stringResource(R.string.app_privacy_policy))
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append(stringResource(R.string.title_termsOfService))
}
@@ -119,7 +116,7 @@ fun LoginHome(
append(" ")
pushStringAnnotation(
tag = "policy",
- annotation = "https://app.getcode.com/privacy-policy"
+ annotation = stringResource(R.string.app_privacy_policy)
)
withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
append(stringResource(R.string.title_privacyPolicy))
@@ -159,15 +156,4 @@ fun LoginHome(
analytics.onAppStarted()
}
-}
-
-@Preview
-@Composable
-private fun Preview_Login() {
- CodeTheme {
- LoginHome(
- createAccount = { },
- login = {}
- )
- }
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/login/NotificationPermission.kt b/app/src/main/java/com/getcode/view/login/NotificationPermission.kt
index 490dc20a9..1252c771b 100644
--- a/app/src/main/java/com/getcode/view/login/NotificationPermission.kt
+++ b/app/src/main/java/com/getcode/view/login/NotificationPermission.kt
@@ -9,15 +9,15 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.constraintlayout.compose.ConstraintLayout
+import com.getcode.AppHomeScreen
import com.getcode.LocalAnalytics
import com.getcode.R
import com.getcode.analytics.Action
import com.getcode.navigation.core.CodeNavigator
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@Composable
fun NotificationPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) {
@@ -27,10 +27,13 @@ fun NotificationPermission(navigator: CodeNavigator = LocalCodeNavigator.current
if (fromOnboarding) {
analytics.action(Action.CompletedOnboarding)
}
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
}
}
- val notificationPermissionCheck = notificationPermissionCheck(onResult = { onNotificationResult(it) })
+ val notificationPermissionCheck =
+ com.getcode.util.permissions.notificationPermissionCheck(onResult = {
+ onNotificationResult(it)
+ })
SideEffect {
notificationPermissionCheck(false)
diff --git a/app/src/main/java/com/getcode/view/login/PhoneConfirm.kt b/app/src/main/java/com/getcode/view/login/PhoneConfirm.kt
index eddb202c2..909cbeb23 100644
--- a/app/src/main/java/com/getcode/view/login/PhoneConfirm.kt
+++ b/app/src/main/java/com/getcode/view/login/PhoneConfirm.kt
@@ -42,13 +42,12 @@ import com.getcode.LocalPhoneFormatter
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.LoginArgs
-import com.getcode.network.repository.replaceParam
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.components.OtpRow
+import com.getcode.utils.replaceParam
internal const val OTP_LENGTH = 6
@@ -144,7 +143,7 @@ fun PhoneConfirm(
} else if (dataState.isResendingCode) {
CodeCircularProgressIndicator(
strokeWidth = CodeTheme.dimens.thickBorder,
- color = BrandLight,
+ color = CodeTheme.colors.brandLight,
modifier = Modifier
.size(CodeTheme.dimens.grid.x4)
.align(Alignment.CenterHorizontally),
diff --git a/app/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt b/app/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt
index a86e119f7..5177db8b4 100644
--- a/app/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt
+++ b/app/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt
@@ -3,26 +3,27 @@ package com.getcode.view.login
import android.annotation.SuppressLint
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
-import com.codeinc.gen.phone.v1.PhoneVerificationService
-import com.codeinc.gen.user.v1.IdentityService
import com.getcode.R
import com.getcode.analytics.Action
import com.getcode.analytics.AnalyticsService
import com.getcode.ed25519.Ed25519
-import com.getcode.manager.MnemonicManager
+import com.getcode.services.manager.MnemonicManager
import com.getcode.manager.SessionManager
import com.getcode.manager.TopBarManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.AccessKeyScreen
import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.PhoneNumberScreen
+import com.getcode.network.repository.CheckVerificationResult
import com.getcode.network.repository.IdentityRepository
+import com.getcode.network.repository.LinkAccountResult
+import com.getcode.network.repository.OtpVerificationResult
import com.getcode.network.repository.PhoneRepository
-import com.getcode.network.repository.encodeBase64
import com.getcode.util.OtpSmsBroadcastReceiver
import com.getcode.util.PhoneUtils
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
+import com.getcode.utils.encodeBase64
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -66,7 +67,7 @@ class PhoneConfirmViewModel @Inject constructor(
private val phoneRepository: PhoneRepository,
private val phoneUtils: PhoneUtils,
private val resources: ResourceHelper,
- private val mnemonicManager: MnemonicManager,
+ private val mnemonicManager: com.getcode.services.manager.MnemonicManager,
) : BaseViewModel(resources) {
companion object {
@@ -194,12 +195,14 @@ class PhoneConfirmViewModel @Inject constructor(
.doOnTerminate { setIsResending(false) }
.doOnComplete { setIsResending(false) }
.observeOn(AndroidSchedulers.mainThread())
- .map { res ->
- when (res) {
- PhoneVerificationService.SendVerificationCodeResponse.Result.OK -> null
- else -> getGenericError()
- }?.let { message -> TopBarManager.showMessage(message) }
- res == PhoneVerificationService.SendVerificationCodeResponse.Result.OK
+ .map { result ->
+ when (result) {
+ is OtpVerificationResult.Error -> {
+ TopBarManager.showMessage(getGenericError())
+ false
+ }
+ OtpVerificationResult.Success -> true
+ }
}
.subscribe({ startTimer() }, ErrorUtils::handleError)
}
@@ -217,17 +220,19 @@ class PhoneConfirmViewModel @Inject constructor(
phoneRepository.checkVerificationCode(phoneNumber, otpInput)
.map { res ->
when (res) {
- PhoneVerificationService.CheckVerificationCodeResponse.Result.OK -> null
- PhoneVerificationService.CheckVerificationCodeResponse.Result.INVALID_CODE ->
- getInvalidCodeError()
-
- PhoneVerificationService.CheckVerificationCodeResponse.Result.NO_VERIFICATION ->
- getTimeoutError()
+ CheckVerificationResult.Error.InvalidCode -> {
+ TopBarManager.showMessage(getInvalidCodeError())
+ }
+ CheckVerificationResult.Error.NoVerification -> {
+ TopBarManager.showMessage(getTimeoutError())
+ }
+ is CheckVerificationResult.Error -> {
+ TopBarManager.showMessage(getGenericError())
+ }
+ CheckVerificationResult.Success -> Unit
+ }
- else ->
- getGenericError()
- }?.let { message -> TopBarManager.showMessage(message) }
- res == PhoneVerificationService.CheckVerificationCodeResponse.Result.OK
+ res == CheckVerificationResult.Success
}.firstOrError()
.doOnSuccess { isSuccess -> if (isSuccess) onOtpValidated() else onOtpError() }
}
@@ -241,14 +246,11 @@ class PhoneConfirmViewModel @Inject constructor(
return identityRepository.linkAccount(keyPair, phoneValue, code)
.map { res ->
when (res) {
- IdentityService.LinkAccountResponse.Result.OK -> null
- IdentityService.LinkAccountResponse.Result.INVALID_TOKEN ->
- getInvalidCodeError()
-
- else ->
- getGenericError()
- }?.let { message -> TopBarManager.showMessage(message) }
- res == IdentityService.LinkAccountResponse.Result.OK
+ LinkAccountResult.Error.InvalidCode -> TopBarManager.showMessage(getInvalidCodeError())
+ is LinkAccountResult.Error -> getGenericError()
+ LinkAccountResult.Success -> Unit
+ }
+ res == LinkAccountResult.Success
}
}
diff --git a/app/src/main/java/com/getcode/view/login/PhoneVerify.kt b/app/src/main/java/com/getcode/view/login/PhoneVerify.kt
index 9c8e35cc3..63ca063d6 100644
--- a/app/src/main/java/com/getcode/view/login/PhoneVerify.kt
+++ b/app/src/main/java/com/getcode/view/login/PhoneVerify.kt
@@ -59,7 +59,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.LoginArgs
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.theme.White05
import com.getcode.theme.White50
@@ -67,8 +66,8 @@ import com.getcode.theme.extraSmall
import com.getcode.util.PhoneUtils
import com.getcode.ui.utils.getActivity
import com.getcode.ui.utils.rememberedClickable
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest
import com.google.android.gms.auth.api.identity.Identity
import kotlinx.coroutines.delay
@@ -200,7 +199,7 @@ private fun PhoneEntry(
modifier = modifier
.border(
width = CodeTheme.dimens.border,
- color = BrandLight,
+ color = CodeTheme.colors.brandLight,
shape = CodeTheme.shapes.extraSmall
)
.background(White05)
@@ -247,7 +246,7 @@ private fun PhoneEntry(
}
Spacer(
modifier = Modifier
- .background(BrandLight)
+ .background(CodeTheme.colors.brandLight)
.width(1.dp)
.fillMaxHeight()
)
diff --git a/app/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt b/app/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt
index 0e5efdb78..5c6b005b0 100644
--- a/app/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt
@@ -4,22 +4,20 @@ import android.annotation.SuppressLint
import android.app.Activity
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
-import com.codeinc.gen.phone.v1.PhoneVerificationService
import com.getcode.R
import com.getcode.analytics.Action
-import com.getcode.analytics.AnalyticsManager
import com.getcode.analytics.AnalyticsService
import com.getcode.manager.TopBarManager
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.LoginPhoneConfirmationScreen
import com.getcode.navigation.screens.PhoneConfirmationScreen
-import com.getcode.network.repository.ErrorSubmitIntent
+import com.getcode.network.repository.OtpVerificationResult
import com.getcode.network.repository.PhoneRepository
import com.getcode.util.PhoneUtils
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
-import com.getcode.utils.makeE164
-import com.getcode.view.*
+import com.getcode.services.utils.makeE164
+import com.getcode.view.BaseViewModel
import com.google.android.gms.auth.api.phone.SmsRetriever
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -178,30 +176,25 @@ class PhoneVerifyViewModel @Inject constructor(
.doOnComplete { setIsLoading(false) }
.map { res ->
val message = when (res) {
- PhoneVerificationService.SendVerificationCodeResponse.Result.OK -> null
+ OtpVerificationResult.Error.InvalidPhoneNumber,
+ OtpVerificationResult.Error.UnsupportedPhoneType -> getUnsupportedPhoneError()
- PhoneVerificationService.SendVerificationCodeResponse.Result.INVALID_PHONE_NUMBER,
- PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_PHONE_TYPE -> {
- getUnsupportedPhoneError()
- }
- PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_COUNTRY -> {
- getUnsupportedCountryError()
- }
- PhoneVerificationService.SendVerificationCodeResponse.Result.UNRECOGNIZED,
- PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_DEVICE -> {
- getUnsupportedDeviceError()
- }
- else -> getGenericError()
+ OtpVerificationResult.Error.UnsupportedCountry -> getUnsupportedCountryError()
+ OtpVerificationResult.Error.UnsupportedDevice -> getUnsupportedDeviceError()
+
+ is OtpVerificationResult.Error -> getGenericError()
+
+ OtpVerificationResult.Success -> null
}
if (message != null) {
TopBarManager.showMessage(message)
}
- val success = res == PhoneVerificationService.SendVerificationCodeResponse.Result.OK
+ val success = res == OtpVerificationResult.Success
if (!success) {
- ErrorUtils.handleError(PhoneVerifyException(reason = res.name))
+ ErrorUtils.handleError(PhoneVerifyException(reason = res::class.java.simpleName))
}
success
diff --git a/app/src/main/java/com/getcode/view/login/SeedDeepLink.kt b/app/src/main/java/com/getcode/view/login/SeedDeepLink.kt
index 878793717..6391e180f 100644
--- a/app/src/main/java/com/getcode/view/login/SeedDeepLink.kt
+++ b/app/src/main/java/com/getcode/view/login/SeedDeepLink.kt
@@ -16,18 +16,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
+import com.getcode.AppHomeScreen
import com.getcode.R
import com.getcode.manager.SessionManager
import com.getcode.manager.TopBarManager
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.CodeLoginPermission
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.LoginScreen
import com.getcode.navigation.screens.PermissionRequestScreen
-import com.getcode.network.repository.decodeBase64
-import com.getcode.network.repository.encodeBase64
-import com.getcode.vendor.Base58
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeCircularProgressIndicator
+import com.getcode.utils.decodeBase64
+import com.getcode.utils.encodeBase64
+import org.kin.sdk.base.tools.Base58
import timber.log.Timber
@Preview
@@ -42,7 +42,7 @@ fun SeedDeepLink(
val authState by SessionManager.authState.collectAsState()
fun navigateMain() {
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
}
fun navigateLogin() = navigator.replace(LoginScreen())
@@ -53,7 +53,8 @@ fun SeedDeepLink(
navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications))
}
}
- val notificationPermissionCheck = notificationPermissionCheck { onNotificationResult(it) }
+ val notificationPermissionCheck =
+ com.getcode.util.permissions.notificationPermissionCheck { onNotificationResult(it) }
fun onError() {
TopBarManager.showMessage(
diff --git a/app/src/main/java/com/getcode/view/login/SeedInput.kt b/app/src/main/java/com/getcode/view/login/SeedInput.kt
index 41cf16e44..0202ea448 100644
--- a/app/src/main/java/com/getcode/view/login/SeedInput.kt
+++ b/app/src/main/java/com/getcode/view/login/SeedInput.kt
@@ -1,6 +1,5 @@
package com.getcode.view.login
-import android.Manifest
import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
@@ -12,7 +11,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -30,7 +28,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.LoginArgs
-import com.getcode.ui.components.*
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@SuppressLint("InlinedApi")
@Preview
@@ -44,7 +43,8 @@ fun SeedInput(
val focusManager = LocalFocusManager.current
val focusRequester = FocusRequester()
- val notificationPermissionCheck = notificationPermissionCheck(isShowError = false) { }
+ val notificationPermissionCheck =
+ com.getcode.util.permissions.notificationPermissionCheck(isShowError = false) { }
Column(
modifier = Modifier
diff --git a/app/src/main/java/com/getcode/view/login/SeedInputViewModel.kt b/app/src/main/java/com/getcode/view/login/SeedInputViewModel.kt
index 2ae7da73b..1b270db48 100644
--- a/app/src/main/java/com/getcode/view/login/SeedInputViewModel.kt
+++ b/app/src/main/java/com/getcode/view/login/SeedInputViewModel.kt
@@ -3,6 +3,7 @@ package com.getcode.view.login
import android.annotation.SuppressLint
import android.app.Activity
import androidx.lifecycle.viewModelScope
+import com.getcode.AppHomeScreen
import dagger.hilt.android.lifecycle.HiltViewModel
import com.getcode.R
import com.getcode.analytics.AnalyticsService
@@ -10,10 +11,9 @@ import com.getcode.crypt.MnemonicPhrase
import com.getcode.manager.AccountManager
import com.getcode.manager.AuthManager
import com.getcode.manager.BottomBarManager
-import com.getcode.manager.MnemonicManager
+import com.getcode.services.manager.MnemonicManager
import com.getcode.manager.TopBarManager
import com.getcode.navigation.core.CodeNavigator
-import com.getcode.navigation.screens.ScanScreen
import com.getcode.navigation.screens.LoginPhoneVerificationScreen
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
@@ -116,7 +116,7 @@ class SeedInputViewModel @Inject constructor(
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
- navigator.replaceAll(ScanScreen())
+ navigator.replaceAll(AppHomeScreen())
}, {
if (it is AuthManager.AuthManagerException.TimelockUnlockedException) {
TopBarManager.showMessage(
diff --git a/app/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt
index b3fd2d5ec..738973bdd 100644
--- a/app/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt
@@ -2,9 +2,9 @@ package com.getcode.view.main.account
import android.annotation.SuppressLint
import androidx.lifecycle.viewModelScope
+import com.getcode.libs.qr.QRCodeGenerator
import com.getcode.media.MediaScanner
-import com.getcode.manager.MnemonicManager
-import com.getcode.navigation.core.CodeNavigator
+import com.getcode.services.manager.MnemonicManager
import com.getcode.util.resources.ResourceHelper
import com.getcode.view.login.BaseAccessKeyViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -23,7 +23,8 @@ class AccountAccessKeyViewModel @Inject constructor(
resources: ResourceHelper,
mnemonicManager: MnemonicManager,
mediaScanner: MediaScanner,
-) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner) {
+ qrCodeGenerator: QRCodeGenerator,
+) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, qrCodeGenerator) {
@SuppressLint("CheckResult")
fun onSubmit() {
Completable.create {
diff --git a/app/src/main/java/com/getcode/view/main/account/AccountDeposit.kt b/app/src/main/java/com/getcode/view/main/account/AccountDeposit.kt
index 986cd597a..4b98b8d54 100644
--- a/app/src/main/java/com/getcode/view/main/account/AccountDeposit.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AccountDeposit.kt
@@ -28,16 +28,15 @@ import androidx.compose.ui.text.style.TextAlign
import com.getcode.R
import com.getcode.manager.SessionManager
import com.getcode.theme.Brand
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.theme.White
import com.getcode.theme.White05
import com.getcode.theme.extraSmall
import com.getcode.ui.utils.rememberedClickable
-import com.getcode.vendor.Base58
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.MiddleEllipsisText
+import org.kin.sdk.base.tools.Base58
@Composable
fun AccountDeposit() {
@@ -70,7 +69,7 @@ fun AccountDeposit() {
modifier = Modifier
.padding(vertical = CodeTheme.dimens.grid.x3)
.clip(CodeTheme.shapes.extraSmall)
- .border(CodeTheme.dimens.border, BrandLight, CodeTheme.shapes.extraSmall)
+ .border(CodeTheme.dimens.border, CodeTheme.colors.brandLight, CodeTheme.shapes.extraSmall)
.fillMaxWidth()
.height(CodeTheme.dimens.grid.x10)
.background(White05)
diff --git a/app/src/main/java/com/getcode/view/main/account/AccountFaq.kt b/app/src/main/java/com/getcode/view/main/account/AccountFaq.kt
index f1b9f012d..6983e9980 100644
--- a/app/src/main/java/com/getcode/view/main/account/AccountFaq.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AccountFaq.kt
@@ -9,9 +9,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.getcode.theme.R as themeR
import com.getcode.theme.CodeTheme
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 70c69390d..7b6461be8 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
@@ -50,7 +50,7 @@ import com.getcode.navigation.screens.FaqScreen
import com.getcode.navigation.screens.WithdrawalAmountScreen
import com.getcode.theme.CodeTheme
import com.getcode.theme.White10
-import com.getcode.ui.components.CodeScaffold
+import com.getcode.ui.theme.CodeScaffold
import com.getcode.ui.utils.getActivity
import com.getcode.ui.utils.rememberedClickable
import com.getcode.ui.utils.verticalScrollStateGradient
@@ -215,7 +215,7 @@ fun ListItem(item: AccountMainItem, onClick: () -> Unit) {
Divider(
modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset),
- color = White10,
+ color = CodeTheme.colors.divider,
thickness = 0.5.dp
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/account/AccountPhone.kt b/app/src/main/java/com/getcode/view/main/account/AccountPhone.kt
index a20f03320..5e59fcbb8 100644
--- a/app/src/main/java/com/getcode/view/main/account/AccountPhone.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AccountPhone.kt
@@ -22,12 +22,12 @@ import com.getcode.manager.BottomBarManager
import com.getcode.manager.SessionManager
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.PhoneVerificationScreen
-import com.getcode.network.repository.urlEncode
import com.getcode.theme.Brand
import com.getcode.theme.CodeTheme
import com.getcode.theme.bolded
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.utils.urlEncode
@Composable
fun AccountPhone(
diff --git a/app/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt
index 958c8ca84..bd2a939c0 100644
--- a/app/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt
@@ -1,12 +1,12 @@
package com.getcode.view.main.account
-import com.codeinc.gen.user.v1.IdentityService
import com.getcode.manager.SessionManager
import com.getcode.network.repository.IdentityRepository
import com.getcode.network.repository.PhoneRepository
+import com.getcode.network.repository.UnlinkAccountResult
import com.getcode.util.PhoneUtils
import com.getcode.util.resources.ResourceHelper
-import com.getcode.utils.makeE164
+import com.getcode.services.utils.makeE164
import com.getcode.view.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,9 +45,10 @@ class AccountPhoneViewModel @Inject constructor(
?.filter { it.isDigit() }?.makeE164() ?: return
identityRepository.unlinkAccount(keyPair, phoneNumber).subscribe { result ->
- if (result == IdentityService.UnlinkAccountResponse.Result.OK)
+ if (result is UnlinkAccountResult.Success) {
phoneRepository.phoneLinked.value = false
- uiFlow.value = AccountPhoneUiModel(false, null)
+ uiFlow.value = AccountPhoneUiModel(false, null)
+ }
}
}
}
\ No newline at end of file
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 fb4ad7653..b59817c1b 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
@@ -7,7 +7,7 @@ import com.getcode.R
import com.getcode.manager.AuthManager
import com.getcode.model.BuyModuleFeature
import com.getcode.model.Feature
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
import com.getcode.network.repository.BetaFlagsRepository
import com.getcode.network.repository.BetaOptions
import com.getcode.network.repository.FeatureRepository
diff --git a/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt b/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt
index 93da5645d..6232caad0 100644
--- a/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt
@@ -10,8 +10,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
-import com.getcode.model.PrefsBool
-import com.getcode.ui.components.SettingsRow
+import com.getcode.services.model.PrefsBool
+import com.getcode.ui.components.SettingsSwitchRow
import com.getcode.util.Biometrics
import kotlinx.coroutines.launch
@@ -27,7 +27,7 @@ fun AppSettingsScreen(
LazyColumn {
items(state.settings) { option ->
if (option.visible) {
- SettingsRow(
+ SettingsSwitchRow(
modifier = Modifier.animateItemPlacement(),
enabled = option.available,
title = stringResource(id = option.name),
diff --git a/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt b/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt
index 510c6de42..e1f2bd8c9 100644
--- a/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt
@@ -1,13 +1,9 @@
package com.getcode.view.main.account
import androidx.lifecycle.viewModelScope
-import com.getcode.R
import com.getcode.mapper.AppSettingsMapper
-import com.getcode.model.APP_SETTINGS
-import com.getcode.model.AppSetting
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.AppSetting
import com.getcode.models.SettingItem
-import com.getcode.network.repository.AppSettings
import com.getcode.network.repository.AppSettingsRepository
import com.getcode.view.BaseViewModel2
import dagger.hilt.android.lifecycle.HiltViewModel
diff --git a/app/src/main/java/com/getcode/view/main/account/BackupKey.kt b/app/src/main/java/com/getcode/view/main/account/BackupKey.kt
index e724c9a29..3a119ea45 100644
--- a/app/src/main/java/com/getcode/view/main/account/BackupKey.kt
+++ b/app/src/main/java/com/getcode/view/main/account/BackupKey.kt
@@ -39,17 +39,17 @@ import androidx.compose.ui.unit.isSpecified
import com.getcode.R
import com.getcode.manager.TopBarManager
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
+import com.getcode.ui.theme.ButtonState
import com.getcode.ui.components.Cloudy
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.PermissionResult
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.SelectionContainer
-import com.getcode.ui.components.getPermissionLauncher
-import com.getcode.ui.components.rememberPermissionChecker
import com.getcode.ui.components.rememberSelectionState
import com.getcode.ui.utils.addIf
import com.getcode.ui.utils.measured
import com.getcode.util.launchAppSettings
+import com.getcode.util.permissions.PermissionResult
+import com.getcode.util.permissions.getPermissionLauncher
+import com.getcode.util.permissions.rememberPermissionHandler
@Composable
fun BackupKey(
@@ -79,7 +79,7 @@ fun BackupKey(
}
val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult)
- val permissionChecker = rememberPermissionChecker()
+ val permissionChecker = rememberPermissionHandler()
if (isExportSeedRequested && isStoragePermissionGranted) {
viewModel.onSubmit()
isExportSeedRequested = false
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 17903baca..bf7df81e6 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
@@ -13,13 +13,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.getcode.R
-import com.getcode.model.Immutable
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.Immutable
+import com.getcode.services.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
-import com.getcode.ui.components.SettingsRow
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.components.SettingsSwitchRow
import dev.bmcreations.tipkit.engines.LocalTipsEngine
@OptIn(ExperimentalFoundationApi::class)
@@ -78,12 +78,6 @@ fun BetaFlagsScreen(
stringResource(id = R.string.beta_photo_gallery_description),
state.galleryEnabled,
),
- BetaFeature(
- PrefsBool.CONVERSATIONS_ENABLED,
- R.string.beta_conversations,
- stringResource(id = R.string.beta_conversations_description),
- state.conversationsEnabled,
- ),
BetaFeature(
PrefsBool.VIBRATE_ON_SCAN,
R.string.beta_vibrate_on_scan,
@@ -154,7 +148,7 @@ fun BetaFlagsScreen(
LazyColumn {
items(options) { option ->
- SettingsRow(
+ SettingsSwitchRow(
modifier = Modifier.animateItemPlacement(),
title = stringResource(id = option.titleResId),
subtitle = option.subtitleText,
@@ -184,7 +178,6 @@ fun BetaFlagsScreen(
private fun BetaOptions.canMutate(flag: PrefsBool): Boolean {
return when (flag) {
is Immutable -> false
- 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 a58b01df1..2fc8b8533 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
@@ -1,9 +1,7 @@
package com.getcode.view.main.account
-import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewModelScope
-import com.getcode.R
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
import com.getcode.network.repository.BetaFlagsRepository
import com.getcode.network.repository.BetaOptions
import com.getcode.network.repository.PrefRepository
diff --git a/app/src/main/java/com/getcode/view/main/account/BucketDebugger.kt b/app/src/main/java/com/getcode/view/main/account/BucketDebugger.kt
index 044fc188f..f27ddd9d2 100644
--- a/app/src/main/java/com/getcode/view/main/account/BucketDebugger.kt
+++ b/app/src/main/java/com/getcode/view/main/account/BucketDebugger.kt
@@ -15,7 +15,6 @@ import com.getcode.manager.SessionManager
import com.getcode.model.displayName
import com.getcode.solana.keys.base58
import com.getcode.solana.organizer.AccountType
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.MiddleEllipsisText
import com.getcode.ui.utils.rememberedClickable
@@ -98,7 +97,7 @@ fun BucketDebugger() {
Spacer(modifier = Modifier
.padding(vertical = CodeTheme.dimens.grid.x2)
- .background(BrandLight)
+ .background(CodeTheme.colors.brandLight)
.fillMaxWidth()
.height(1.dp))
}
diff --git a/app/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt b/app/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt
index be1a04ac8..7e8b1bb4d 100644
--- a/app/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt
+++ b/app/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt
@@ -22,8 +22,8 @@ import com.getcode.theme.CodeTheme
import com.getcode.theme.extraSmall
import com.getcode.theme.inputColors
import com.getcode.ui.utils.getActivity
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@OptIn(ExperimentalComposeUiApi::class)
@Composable
diff --git a/app/src/main/java/com/getcode/view/main/account/DeleteAccount.kt b/app/src/main/java/com/getcode/view/main/account/DeleteAccount.kt
index d4b406b06..6ba905e39 100644
--- a/app/src/main/java/com/getcode/view/main/account/DeleteAccount.kt
+++ b/app/src/main/java/com/getcode/view/main/account/DeleteAccount.kt
@@ -14,8 +14,8 @@ import com.getcode.R
import com.getcode.navigation.screens.DeleteConfirmationScreen
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.TextSection
@Composable
diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt
index da1e87b8c..3883bd9cf 100644
--- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt
+++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt
@@ -18,7 +18,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -29,8 +28,8 @@ import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.WithdrawalArgs
import com.getcode.theme.CodeTheme
import com.getcode.theme.Success
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.TextInput
@OptIn(ExperimentalFoundationApi::class)
diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt
index 0077bc2dc..31d02b6b6 100644
--- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt
@@ -3,9 +3,9 @@ package com.getcode.view.main.account.withdraw
import android.annotation.SuppressLint
import android.content.ClipboardManager
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
-import androidx.compose.foundation.text2.input.textAsFlow
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
+import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.viewModelScope
import com.getcode.navigation.core.CodeNavigator
import com.getcode.navigation.screens.WithdrawalArgs
@@ -44,7 +44,6 @@ data class AccountWithdrawAddressUiModel(
)
@HiltViewModel
-@OptIn(ExperimentalFoundationApi::class)
class AccountWithdrawAddressViewModel @Inject constructor(
private val client: Client,
private val clipboard: ClipboardManager,
@@ -55,7 +54,7 @@ class AccountWithdrawAddressViewModel @Inject constructor(
init {
uiFlow.map { it.addressText }
- .flatMapLatest { it.textAsFlow() }
+ .flatMapLatest { snapshotFlow { it.text } }
.map { it.toString() }
.debounce(300.milliseconds)
.onEach { updated -> setAddress(updated) }
diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt
index 39bf42a97..23d7eeda6 100644
--- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt
+++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt
@@ -15,12 +15,11 @@ import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.CurrencySelectionModal
import com.getcode.theme.Alert
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeKeyPad
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.ui.components.text.AmountArea
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeKeyPad
@Composable
fun AccountWithdrawAmount(
@@ -36,7 +35,7 @@ fun AccountWithdrawAmount(
horizontalAlignment = Alignment.CenterHorizontally
) {
val color =
- if (dataState.amountModel.balanceKin < dataState.amountModel.amountKin.toKinValueDouble()) Alert else BrandLight
+ if (dataState.amountModel.balanceKin < dataState.amountModel.amountKin.toKinValueDouble()) CodeTheme.colors.errorText else CodeTheme.colors.brandLight
Box(
modifier = Modifier.weight(0.5f)
diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt
index 2fabac7f1..3d0dc2ab3 100644
--- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt
@@ -11,12 +11,9 @@ import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.locale.LocaleHelper
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.view.main.giveKin.AmountAnimatedInputUiModel
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import com.getcode.view.main.giveKin.AmountUiModel
import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel
import com.getcode.view.main.giveKin.CurrencyUiModel
@@ -42,9 +39,9 @@ class AccountWithdrawAmountViewModel @Inject constructor(
prefsRepository: PrefRepository,
balanceRepository: BalanceRepository,
transactionRepository: TransactionRepository,
- localeHelper: LocaleHelper,
- currencyUtils: CurrencyUtils,
- networkObserver: NetworkConnectivityListener,
+ localeHelper: com.getcode.util.locale.LocaleHelper,
+ currencyUtils: com.getcode.utils.CurrencyUtils,
+ networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
resources: ResourceHelper,
) : BaseAmountCurrencyViewModel(
client,
diff --git a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt
index d2d28543c..d122ecb2f 100644
--- a/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt
+++ b/app/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt
@@ -3,9 +3,19 @@ package com.getcode.view.main.account.withdraw
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -15,12 +25,10 @@ import androidx.constraintlayout.compose.ConstraintLayout
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.WithdrawalArgs
-import com.getcode.theme.Brand01
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.ui.components.text.AmountArea
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@Composable
fun AccountWithdrawSummary(
@@ -49,8 +57,8 @@ fun AccountWithdrawSummary(
) {
Box(
modifier = Modifier
- .border(width = CodeTheme.dimens.border, color = BrandLight, shape = CodeTheme.shapes.medium)
- .background(Brand01)
+ .border(width = CodeTheme.dimens.border, color = CodeTheme.colors.brandLight, shape = CodeTheme.shapes.medium)
+ .background(CodeTheme.colors.brandDark)
.padding(CodeTheme.dimens.grid.x4)
) {
AmountArea(
@@ -73,8 +81,8 @@ fun AccountWithdrawSummary(
Box(
modifier = Modifier
- .border(width = CodeTheme.dimens.border, color = BrandLight, shape = CodeTheme.shapes.medium)
- .background(Brand01)
+ .border(width = CodeTheme.dimens.border, color = CodeTheme.colors.brandLight, shape = CodeTheme.shapes.medium)
+ .background(CodeTheme.colors.brandDark)
.padding(CodeTheme.dimens.grid.x4)
) {
Text(
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 1720b81f9..0f91fe859 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
@@ -4,8 +4,8 @@ import android.annotation.SuppressLint
import androidx.lifecycle.viewModelScope
import com.getcode.R
import com.getcode.analytics.AnalyticsService
-import com.getcode.solana.keys.PublicKey
import com.getcode.manager.BottomBarManager
+import com.getcode.solana.keys.PublicKey
import com.getcode.manager.SessionManager
import com.getcode.manager.TopBarManager
import com.getcode.model.CurrencyCode
@@ -19,12 +19,12 @@ import com.getcode.network.NotificationCollectionHistoryController
import com.getcode.network.client.*
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
-import com.getcode.vendor.Base58
import com.getcode.view.*
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
+import org.kin.sdk.base.tools.Base58
import javax.inject.Inject
private const val TAG = "AccountWithdrawSummaryViewModel"
@@ -105,7 +105,8 @@ class AccountWithdrawSummaryViewModel @Inject constructor(
)
val organizer = SessionManager.getOrganizer() ?: return
- val destination = PublicKey(Base58.decode(uiModel.resolvedDestination).toList())
+ val destination =
+ PublicKey(Base58.decode(uiModel.resolvedDestination).toList())
client.withdrawExternally(amount, organizer, destination)
.observeOn(AndroidSchedulers.mainThread())
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 be30f4b38..b38474f4a 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
@@ -33,31 +33,27 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import cafe.adriel.voyager.navigator.currentOrThrow
-import com.getcode.LocalSession
import com.getcode.R
import com.getcode.manager.TopBarManager
import com.getcode.model.Currency
import com.getcode.model.CurrencyCode
import com.getcode.model.Rate
import com.getcode.model.chat.Chat
-import com.getcode.model.chat.isConversation
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.BuyMoreKinModal
-import com.getcode.navigation.screens.ConversationScreen
import com.getcode.navigation.screens.NotificationCollectionScreen
import com.getcode.navigation.screens.CurrencySelectionModal
import com.getcode.navigation.screens.FaqScreen
import com.getcode.theme.CodeTheme
+import com.getcode.theme.DesignSystem
import com.getcode.theme.White10
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeCircularProgressIndicator
-import com.getcode.ui.components.chat.ChatNode
-import com.getcode.util.Kin
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeCircularProgressIndicator
+import com.getcode.utils.Kin
import com.getcode.view.main.account.BucketDebugger
import com.getcode.view.main.currency.CurrencySelectKind
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.ui.components.text.AmountArea
@Composable
@@ -185,7 +181,7 @@ fun BalanceContent(
ChatNode(chat = chat, onClick = { openChat(chat) })
Divider(
modifier = Modifier.padding(start = CodeTheme.dimens.inset),
- color = White10,
+ color = CodeTheme.colors.divider,
)
}
@@ -363,7 +359,7 @@ private fun EmptyTransactionsHint(faqOpen: () -> Unit) {
@Preview
@Composable
private fun TopPreview() {
- CodeTheme {
+ DesignSystem {
val model = BalanceSheetViewModel.State(
amountText = "$12.34 of Kin",
marketValue = 2_225_100.0,
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 1ab626dc6..3ab43ddb1 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
@@ -6,20 +6,17 @@ import com.getcode.model.BuyModuleFeature
import com.getcode.model.chat.Chat
import com.getcode.model.Currency
import com.getcode.model.Feature
-import com.getcode.model.PrefsBool
+import com.getcode.services.model.PrefsBool
import com.getcode.model.Rate
import com.getcode.network.BalanceController
import com.getcode.network.NotificationCollectionHistoryController
import com.getcode.network.repository.FeatureRepository
import com.getcode.network.repository.PrefRepository
-import com.getcode.util.Kin
-import com.getcode.utils.network.NetworkConnectivityListener
+import com.getcode.utils.Kin
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
import kotlinx.coroutines.flow.map
@@ -33,7 +30,7 @@ class BalanceSheetViewModel @Inject constructor(
history: NotificationCollectionHistoryController,
prefsRepository: PrefRepository,
features: FeatureRepository,
- networkObserver: NetworkConnectivityListener,
+ networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
) : BaseViewModel2(
initialState = State(),
updateStateForEvent = updateStateForEvent
@@ -122,12 +119,6 @@ class BalanceSheetViewModel @Inject constructor(
}.onEach {
dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(false))
}.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .filter { features.isEnabled(PrefsBool.CONVERSATIONS_ENABLED) }
- .onEach { history.fetch(true) }
- .launchIn(viewModelScope)
}
companion object {
diff --git a/app/src/main/java/com/getcode/view/main/balance/ChatNode.kt b/app/src/main/java/com/getcode/view/main/balance/ChatNode.kt
new file mode 100644
index 000000000..aa0b08bcc
--- /dev/null
+++ b/app/src/main/java/com/getcode/view/main/balance/ChatNode.kt
@@ -0,0 +1,46 @@
+package com.getcode.view.main.balance
+
+import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.AnnotatedString
+import com.getcode.model.chat.Chat
+import com.getcode.model.chat.MessageContent
+import com.getcode.ui.components.chat.ChatNode
+import com.getcode.ui.components.chat.utils.localized
+import com.getcode.ui.components.chat.utils.localizedText
+
+@Composable
+fun ChatNode(
+ modifier: Modifier = Modifier,
+ chat: Chat,
+ showAvatar: Boolean = false,
+ onClick: () -> Unit,
+) {
+ ChatNode(
+ modifier = modifier,
+ title = chat.title.localized,
+ messagePreview = chat.messagePreview,
+ avatar = if (showAvatar) chat.imageData else null,
+ timestamp = chat.lastMessageMillis,
+ isMuted = chat.isMuted,
+ unreadCount = chat.unreadCount,
+ onClick = onClick
+ )
+}
+
+private val Chat.messagePreview: Pair>
+ @Composable get() {
+ val contents = newestMessage?.contents ?: return AnnotatedString("No content") to emptyMap()
+
+ var filtered: List = contents.filterIsInstance()
+ if (filtered.isEmpty()) {
+ filtered = contents
+ }
+
+ // joinToString does expose a Composable scoped lambda
+ @Suppress("SimplifiableCallChain")
+ val messageBody = filtered.map { it.localizedText }.joinToString(" ")
+
+ return AnnotatedString(messageBody) to emptyMap()
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt b/app/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt
index 220b6a381..102528977 100644
--- a/app/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt
@@ -15,7 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.getcode.models.Bill
-import com.getcode.ui.components.CustomSwipeToDismiss
+import com.getcode.ui.theme.CustomSwipeToDismiss
@OptIn(ExperimentalMaterialApi::class)
@Composable
diff --git a/app/src/main/java/com/getcode/view/main/bill/Bill.kt b/app/src/main/java/com/getcode/view/main/bill/Bill.kt
index ca6c8bacb..4c0b6cfb5 100644
--- a/app/src/main/java/com/getcode/view/main/bill/Bill.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/Bill.kt
@@ -6,13 +6,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
-import com.getcode.model.CodePayload
+import com.getcode.services.model.CodePayload
import com.getcode.model.CurrencyCode
import com.getcode.model.Fiat
import com.getcode.model.KinAmount
-import com.getcode.model.Kind
+import com.getcode.services.model.Kind
+import com.getcode.model.fromFiatAmount
import com.getcode.models.Bill
-import com.getcode.theme.CodeTheme
+import com.getcode.theme.DesignSystem
@Composable
fun Bill(
@@ -50,7 +51,7 @@ fun Bill(
@Preview
@Composable
fun Preview_CashBill() {
- CodeTheme {
+ DesignSystem {
val payload = CodePayload(
Kind.Cash,
value = Fiat(CurrencyCode.USD, 3.00),
@@ -73,7 +74,7 @@ fun Preview_CashBill() {
@Preview
@Composable
fun Preview_PaymentBill() {
- CodeTheme {
+ DesignSystem {
val payload = CodePayload(
Kind.RequestPayment,
value = Fiat(CurrencyCode.USD, 0.25),
diff --git a/app/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt b/app/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt
index af81434b2..01dbf3022 100644
--- a/app/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt
@@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp
import com.getcode.models.BillState
import com.getcode.theme.CodeTheme
import com.getcode.theme.White
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.components.Pill
import com.getcode.ui.utils.rememberedClickable
diff --git a/app/src/main/java/com/getcode/view/main/bill/CashBill.kt b/app/src/main/java/com/getcode/view/main/bill/CashBill.kt
index 77da6a1fb..50ff18dba 100644
--- a/app/src/main/java/com/getcode/view/main/bill/CashBill.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/CashBill.kt
@@ -60,7 +60,7 @@ import com.getcode.ui.utils.drawWithGradient
import com.getcode.ui.utils.nonScaledSp
import com.getcode.ui.utils.punchCircle
import com.getcode.ui.utils.punchRectangle
-import com.getcode.util.formattedRaw
+import com.getcode.extensions.formattedRaw
import kotlin.math.ceil
import kotlin.math.roundToInt
diff --git a/app/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt b/app/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt
index aacd09fd1..7936e1666 100644
--- a/app/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt
@@ -32,7 +32,7 @@ import com.getcode.theme.CodeTheme
import com.getcode.theme.DashEffect
import com.getcode.theme.receipt
import com.getcode.theme.monospace
-import com.getcode.view.main.scanner.components.PriceWithFlag
+import com.getcode.ui.components.PriceWithFlag
@OptIn(ExperimentalLayoutApi::class)
@Composable
diff --git a/app/src/main/java/com/getcode/view/main/bill/TipCard.kt b/app/src/main/java/com/getcode/view/main/bill/TipCard.kt
index c3fe267f5..bfe1ce175 100644
--- a/app/src/main/java/com/getcode/view/main/bill/TipCard.kt
+++ b/app/src/main/java/com/getcode/view/main/bill/TipCard.kt
@@ -29,17 +29,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.inset
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -51,12 +45,11 @@ import com.getcode.R
import com.getcode.theme.Brand
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.CardFace
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.components.FlippableCard
import com.getcode.ui.components.Row
import com.getcode.ui.components.TwitterUsernameDisplay
import com.getcode.ui.utils.Geometry
-import com.getcode.ui.utils.rememberedLongClickable
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.haze
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 ddee65658..1ea6e5b05 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
@@ -15,8 +15,8 @@ import androidx.compose.ui.unit.Dp
import androidx.paging.compose.LazyPagingItems
import com.getcode.R
import com.getcode.manager.BottomBarManager
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.components.Row
import com.getcode.ui.components.VerticalDivider
import com.getcode.ui.components.chat.MessageList
@@ -41,12 +41,6 @@ fun ChatScreen(
modifier = Modifier.weight(1f),
listState = listState,
messages = messages,
- dispatch = {
- when (it) {
- is MessageListEvent.OpenMessageChat -> dispatch(NotificationCollectionViewModel.Event.OpenMessageChat(it.reference))
- is MessageListEvent.AdvancePointer -> Unit // handled on conversation open
- }
- }
)
Row(
diff --git a/app/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt
index 84a1aaf4d..682e9bdb4 100644
--- a/app/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt
@@ -5,20 +5,20 @@ import androidx.paging.flatMap
import androidx.paging.insertSeparators
import androidx.paging.map
import com.getcode.model.ID
-import com.getcode.model.MessageStatus
import com.getcode.model.chat.MessageContent
+import com.getcode.model.chat.MessageStatus
import com.getcode.model.chat.NotificationCollectionEntity
import com.getcode.model.chat.Reference
+import com.getcode.model.chat.Sender
import com.getcode.model.chat.Title
import com.getcode.model.chat.Verb
-import com.getcode.network.ConversationController
import com.getcode.network.NotificationCollectionHistoryController
import com.getcode.network.repository.BetaFlagsRepository
-import com.getcode.network.repository.base58
import com.getcode.ui.components.chat.utils.ChatItem
import com.getcode.ui.components.chat.utils.ChatMessageIndice
import com.getcode.util.formatDateRelatively
import com.getcode.util.toInstantFromMillis
+import com.getcode.utils.base58
import com.getcode.view.BaseViewModel2
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -41,7 +41,6 @@ import javax.inject.Inject
@HiltViewModel
class NotificationCollectionViewModel @Inject constructor(
historyController: NotificationCollectionHistoryController,
- conversationController: ConversationController,
betaFlags: BetaFlagsRepository,
) : BaseViewModel2(
initialState = State(
@@ -160,30 +159,26 @@ class NotificationCollectionViewModel @Inject constructor(
}
.mapLatest { page ->
page.map { (message, contents) ->
- val content =
- if (contents is MessageContent.Exchange && contents.verb is Verb.ReceivedTip) {
- val hasMessaged = conversationController.hasInteracted(message.id)
- contents.copy(hasInteracted = hasMessaged)
- } else {
- contents
- }
-
ChatItem.Message(
chatMessageId = message.id,
- message = content,
+ message = contents,
date = message.dateMillis.toInstantFromMillis(),
- status = if (message.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown,
- isFromSelf = message.isFromSelf
+ status = if (contents.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown,
+ sender = Sender(
+ id = null,
+ displayName = null,
+ profileImage = null,
+ isHost = false,
+ isSelf = contents.isFromSelf,
+
+ )
)
}
}
.mapLatest { page ->
page.insertSeparators { before: ChatItem.Message?, after: ChatItem.Message? ->
- val beforeDate = before?.date?.formatDateRelatively()
- val afterDate = after?.date?.formatDateRelatively()
-
- if (beforeDate != afterDate) {
- beforeDate?.let { ChatItem.Date(it) }
+ if (before?.date != after?.date) {
+ before?.date?.let { ChatItem.Date(it) }
} else {
null
}
diff --git a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationScreen.kt b/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationScreen.kt
deleted file mode 100644
index f128eb354..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationScreen.kt
+++ /dev/null
@@ -1,204 +0,0 @@
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package com.getcode.view.main.chat.conversation
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.scaleIn
-import androidx.compose.animation.scaleOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.text.ClickableText
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.neverEqualPolicy
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.font.FontWeight
-import androidx.paging.compose.LazyPagingItems
-import cafe.adriel.voyager.navigator.currentOrThrow
-import com.getcode.LocalSession
-import com.getcode.R
-import com.getcode.SessionEvent
-import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.navigation.screens.ConnectAccount
-import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeScaffold
-import com.getcode.ui.components.chat.utils.ChatItem
-import com.getcode.ui.components.chat.ChatInput
-import com.getcode.ui.components.chat.MessageList
-import com.getcode.ui.components.chat.MessageListEvent
-import com.getcode.ui.components.chat.TypingIndicator
-import com.getcode.ui.components.chat.utils.HandleMessageChanges
-import com.getcode.util.formatted
-import com.getcode.view.main.tip.IdentityConnectionReason
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import java.util.UUID
-
-@Composable
-fun ConversationScreen(
- state: ConversationViewModel.State,
- messages: LazyPagingItems,
- dispatchEvent: (ConversationViewModel.Event) -> Unit,
-) {
- val navigator = LocalCodeNavigator.current
-
- CodeScaffold(
- topBar = {
- IdentityRevealHeader(state = state) {
- if (state.identityAvailable) {
- dispatchEvent(ConversationViewModel.Event.RevealIdentity)
- } else {
- navigator.push(ConnectAccount(IdentityConnectionReason.IdentityReveal))
- }
- }
- },
- bottomBar = {
- Column(
- modifier = Modifier.imePadding(),
- verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3),
- ) {
- val canChat = remember(state.twitterUser) {
- state.twitterUser == null || state.twitterUser.isFriend
- }
- if (canChat) {
- AnimatedVisibility(
- visible = state.showTypingIndicator,
- enter = slideInVertically(
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- )
- ) { it } + scaleIn() + fadeIn(),
- exit = fadeOut() + scaleOut() + slideOutVertically { it }
- ) {
- TypingIndicator(
- modifier = Modifier
- .padding(horizontal = CodeTheme.dimens.grid.x2)
- )
- }
- ChatInput(
- state = state.textFieldState,
- sendCashEnabled = state.tipChatCash.enabled,
- onSendMessage = { dispatchEvent(ConversationViewModel.Event.SendMessage) },
- onSendCash = { dispatchEvent(ConversationViewModel.Event.SendCash) }
- )
- } else {
- CodeButton(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = CodeTheme.dimens.grid.x2)
- .padding(horizontal = CodeTheme.dimens.inset),
- buttonState = ButtonState.Filled,
- text = stringResource(
- R.string.action_payToChat,
- state.costToChat.formatted(suffix = "")
- )
- ) {
- dispatchEvent(ConversationViewModel.Event.PresentPaymentConfirmation)
- }
- }
- }
- }
- ) { padding ->
- val lazyListState = rememberLazyListState()
- MessageList(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding),
- messages = messages,
- listState = lazyListState,
- dispatch = { event ->
- if (event is MessageListEvent.AdvancePointer) {
- dispatchEvent(ConversationViewModel.Event.MarkRead(event.messageId))
- }
- }
- )
-
- HandleMessageChanges(listState = lazyListState, items = messages) { message ->
- dispatchEvent(ConversationViewModel.Event.MarkDelivered(message.chatMessageId))
- }
- }
-}
-
-@Composable
-private fun IdentityRevealHeader(
- state: ConversationViewModel.State,
- onClick: () -> Unit
-) {
- var showRevealHeader by remember {
- mutableStateOf(false)
- }
-
- LaunchedEffect(state.identityRevealed, state.users) {
- if (state.identityRevealed == false) {
- delay(500)
- }
- showRevealHeader = state.identityRevealed == false
- }
-
- AnimatedContent(
- targetState = showRevealHeader,
- label = "show/hide identity reveal header"
- ) { show ->
- if (show) {
- Surface(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = CodeTheme.dimens.grid.x2),
- color = CodeTheme.colors.background,
- ) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "Your messages are showing up anonymously.",
- style = CodeTheme.typography.linkSmall.copy(
- textDecoration = null,
- fontWeight = FontWeight.W700),
- )
-
- ClickableText(
- text = AnnotatedString("Tap to Reveal Your Identity"),
- style = CodeTheme.typography.linkSmall.copy(
- color = Color.White,
- fontWeight = FontWeight.W700
- ),
- ) {
- onClick()
- }
- }
- }
- }
- }
-
-}
-
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
deleted file mode 100644
index ed7bb423e..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt
+++ /dev/null
@@ -1,575 +0,0 @@
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package com.getcode.view.main.chat.conversation
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.clearText
-import androidx.compose.foundation.text2.input.textAsFlow
-import androidx.lifecycle.viewModelScope
-import androidx.paging.PagingData
-import androidx.paging.flatMap
-import androidx.paging.map
-import com.getcode.BuildConfig
-import com.getcode.R
-import com.getcode.SessionController
-import com.getcode.SessionEvent
-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.ConversationCashFeature
-import com.getcode.model.KinAmount
-import com.getcode.model.TwitterUser
-import com.getcode.model.chat.Platform
-import com.getcode.model.chat.Reference
-import com.getcode.model.uuid
-import com.getcode.network.ConversationController
-import com.getcode.network.TipController
-import com.getcode.network.exchange.Exchange
-import com.getcode.network.repository.FeatureRepository
-import com.getcode.ui.components.chat.utils.ChatItem
-import com.getcode.ui.components.chat.utils.ConversationMessageIndice
-import com.getcode.util.resources.ResourceHelper
-import com.getcode.util.toInstantFromMillis
-import com.getcode.utils.ErrorUtils
-import com.getcode.utils.TraceType
-import com.getcode.utils.timestamp
-import com.getcode.utils.trace
-import com.getcode.view.BaseViewModel2
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.asFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.distinctUntilChangedBy
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-import kotlinx.datetime.Instant
-import timber.log.Timber
-import java.util.UUID
-import javax.inject.Inject
-
-@HiltViewModel
-class ConversationViewModel @Inject constructor(
- private val conversationController: ConversationController,
- features: FeatureRepository,
- tipController: TipController,
- exchange: Exchange,
- resources: ResourceHelper,
- sessionController: SessionController,
-) : BaseViewModel2(
- initialState = State.Default,
- updateStateForEvent = updateStateForEvent
-) {
-
- data class State(
- val conversationId: ID?,
- val twitterUser: TwitterUser?,
- val costToChat: KinAmount,
- val reference: Reference.IntentId?,
- val textFieldState: TextFieldState,
- val tipChatCash: Feature,
- val identityAvailable: Boolean,
- val identityRevealed: Boolean?,
- val users: List,
- val lastSeen: Instant?,
- val pointers: Map,
- val showTypingIndicator: Boolean,
- val isSelfTyping: Boolean,
- ) {
- data class User(
- val memberId: UUID,
- val username: String?,
- val imageUrl: String?,
- ) {
- val isRevealed: Boolean
- get() = username != null
- }
-
- companion object {
- val Default = State(
- twitterUser = null,
- costToChat = KinAmount.Zero,
- conversationId = null,
- reference = null,
- tipChatCash = ConversationCashFeature(),
- textFieldState = TextFieldState(),
- identityAvailable = false,
- identityRevealed = null,
- users = emptyList(),
- lastSeen = null,
- pointers = emptyMap(),
- showTypingIndicator = false,
- isSelfTyping = false,
- )
- }
- }
-
- sealed interface Event {
- data class OnTwitterUserChanged(val user: TwitterUser?) : Event
- data class OnCostToChatChanged(val cost: KinAmount) : Event
- data class OnMembersChanged(val members: List) : Event
- data class OnChatIdChanged(val chatId: ID?) : Event
- data class OnConversationChanged(val conversationWithPointers: ConversationWithLastPointers) :
- Event
-
- data class OnUserRevealed(
- val memberId: UUID,
- val username: String? = null,
- val imageUrl: String? = null,
- ) : Event
-
- data class OnTipsChatCashChanged(val module: Feature) : Event
-
- data class OnUserActivity(val activity: Instant) : Event
- data object SendCash : Event
- data object SendMessage : Event
- data object RevealIdentity : Event
-
- data class OnIdentityAvailable(val available: Boolean) : Event
- data object OnIdentityRevealed : Event
-
- data class OnPointersUpdated(val pointers: Map) : Event
- data class MarkRead(val messageId: ID) : Event
- data class MarkDelivered(val messageId: ID) : Event
-
- data object PresentPaymentConfirmation : Event
-
- data object OnTypingStarted: Event
- data object OnTypingStopped: Event
-
- data object OnUserTypingStarted: Event
- data object OnUserTypingStopped: Event
-
- data class Error(val fatal: Boolean, val message: String = "", val show: Boolean = true) :
- Event
- }
-
- init {
- // this is an existing conversation so we fetch the chat directly
- eventFlow
- .filterIsInstance()
- .map { it.chatId }
- .filterNotNull()
- .mapNotNull {
- conversationController.getConversation(it)
- }.onEach {
- dispatchEvent(Event.OnConversationChanged(it))
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .map { it.user }
- .filterNotNull()
- .mapNotNull { user ->
- val currencySymbol = user.costOfFriendship.currency
- val rate = exchange.rateFor(currencySymbol) ?: exchange.rateForUsd()!!
-
- user to KinAmount.fromFiatAmount(fiat = user.costOfFriendship, rate = rate)
- }.map { (user, cost) ->
- dispatchEvent(Event.OnCostToChatChanged(cost))
- user
- }.onEach { user ->
- val member = user.let {
- State.User(
- memberId = UUID.randomUUID(),
- username = user.username,
- imageUrl = user.imageUrl
- )
- }
-
- dispatchEvent(Event.OnMembersChanged(listOf(member)))
- }
- .launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .mapNotNull {
- val state = stateFlow.value
- if (state.twitterUser == null) return@mapNotNull null
- state.twitterUser to state.costToChat
- }.onEach { (user, amount) ->
- sessionController.presentPrivatePaymentConfirmation(
- socialUser = user,
- amount = amount
- )
- }.launchIn(viewModelScope)
-
- sessionController.eventFlow
- .filterIsInstance()
- .onEach { event ->
- runCatching {
- val conversation = conversationController.getOrCreateConversation(
- identifier = event.intentId,
- with = event.user
- )
- dispatchEvent(Event.OnConversationChanged(conversation))
- }.onFailure {
- it.printStackTrace()
- TopBarManager.showMessage(
- "Failed to Start Chat",
- "We were unable to start a chat with ${event.user.username}. Please try again.",
- )
-
- dispatchEvent(
- Event.Error(
- message = if (BuildConfig.DEBUG) it.message.orEmpty() else "Failed to create conversation",
- show = false,
- fatal = true
- )
- )
- }.getOrNull()
- }
- .launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .map { it.conversationWithPointers }
- .distinctUntilChangedBy { it.conversation.id }
- .onEach { conversationController.resetUnreadCount(it.conversation.id) }
- .onEach { (conversation, _) ->
- runCatching {
- conversationController.openChatStream(viewModelScope, conversation)
- }.onFailure {
- it.printStackTrace()
- ErrorUtils.handleError(it)
- }
- }.flatMapLatest { (conversation, _) ->
- conversationController.observeConversation(conversation.id)
- }.filterNotNull()
- .distinctUntilChanged()
- .onEach { dispatchEvent(Event.OnConversationChanged(it)) }
- .launchIn(viewModelScope)
-
- features.conversationsCash
- .onEach { dispatchEvent(Event.OnTipsChatCashChanged(it)) }
- .launchIn(viewModelScope)
-
- tipController.connectedAccount
- .onEach {
- dispatchEvent(Event.OnIdentityAvailable(it != null))
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .map { it.messageId }
- .filter { stateFlow.value.conversationId != null }
- .map { it to stateFlow.value.conversationId!! }
- .onEach { (messageId, conversationId) ->
- conversationController.advanceReadPointer(
- conversationId,
- messageId,
- MessageStatus.Read
- )
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .map { it.messageId }
- .filter { stateFlow.value.conversationId != null }
- .map { it to stateFlow.value.conversationId!! }
- .onEach { (messageId, conversationId) ->
- conversationController.advanceReadPointer(
- conversationId,
- messageId,
- MessageStatus.Delivered
- )
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .map { stateFlow.value }
- .onEach {
- val textFieldState = it.textFieldState
- val text = textFieldState.text.toString()
- textFieldState.clearText()
-
- conversationController.sendMessage(it.conversationId!!, text)
- .onSuccess {
- trace(
- tag = "Conversation",
- message = "message sent successfully",
- type = TraceType.Silent
- )
- }
- .onFailure { error ->
- trace(
- tag = "Conversation",
- message = "message failed to send",
- type = TraceType.Error,
- error = error
- )
- }
- }
- .launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .mapNotNull { stateFlow.value.conversationId }
- .onEach { conversationId ->
- val user = stateFlow.value.users.firstOrNull()?.username ?: "This user"
- val identity = tipController.connectedAccount.value ?: return@onEach
- val platform = when (identity) {
- is TwitterUser -> Platform.Twitter
- }
- BottomBarManager.showMessage(
- BottomBarManager.BottomBarMessage(
- title = resources.getString(R.string.prompt_title_revealIdentity),
- subtitle = resources.getString(
- R.string.prompt_subtitle_revealIdentity,
- user,
- identity.username
- ),
- positiveText = resources.getString(R.string.action_yes),
- type = BottomBarManager.BottomBarMessageType.REMOTE_SEND,
- onPositive = {
- viewModelScope.launch {
- conversationController.revealIdentity(
- conversationId,
- platform,
- identity.username
- ).onSuccess { dispatchEvent(Event.OnIdentityRevealed) }
- .onFailure { it.printStackTrace() }
- }
- },
- negativeText = resources.getString(R.string.action_nevermind)
- )
- )
- }
- .launchIn(viewModelScope)
-
- stateFlow
- .mapNotNull { it.users }
- .distinctUntilChanged()
- .flatMapLatest { users ->
- users.asFlow() // Convert the list to a flow
- }
- .filter { it.username != null }
- .mapNotNull { user ->
- val username = user.username ?: return@mapNotNull null
- runCatching { tipController.fetch(username) }
- .getOrNull() to user
- }
- .onEach { (result, user) ->
- if (result != null) {
- dispatchEvent(
- Event.OnUserRevealed(
- memberId = user.memberId,
- result.username,
- result.imageUrl
- )
- )
- }
- }
- .launchIn(viewModelScope)
-
- stateFlow
- .map { it.conversationId }
- .filterNotNull()
- .distinctUntilChanged()
- .flatMapLatest { conversationController.observeTyping(it) }
- .onEach { isOtherUserTyping ->
- if (isOtherUserTyping) {
- dispatchEvent(Event.OnTypingStarted)
- } else {
- dispatchEvent(Event.OnTypingStopped)
- }
- }.launchIn(viewModelScope)
-
-
- stateFlow
- .map { it.textFieldState }
- .flatMapLatest { it.textAsFlow() }
- .onEach {
- if (it.isEmpty()) {
- dispatchEvent(Event.OnUserTypingStopped)
- } else if (it.isNotEmpty()) {
- if (!stateFlow.value.isSelfTyping) {
- dispatchEvent(Event.OnUserTypingStarted)
- }
- }
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .mapNotNull { stateFlow.value.conversationId }
- .onEach {
- conversationController.onUserStartedTypingIn(it)
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .mapNotNull { stateFlow.value.conversationId }
- .onEach {
- conversationController.onUserStoppedTypingIn(it)
- }.launchIn(viewModelScope)
- }
-
- val messages: Flow> = stateFlow
- .map { it.conversationId }
- .filterNotNull()
- .flatMapLatest { conversationController.conversationPagingData(it) }
- .map { page ->
- page.flatMap { mwc ->
- mwc.contents.map { ConversationMessageIndice(mwc.message, it) }
- }
- }
- .map { page ->
- page.map { indice ->
- val (message, contents) = indice
-
- val pointers = stateFlow.value.pointers
- val pointerRefs = pointers
- .mapKeys { it.key.timestamp }
- .filterKeys { it != null }
- .mapKeys { it.key!! }
-
- val messageTimestamp = message.id.uuid?.timestamp
-
- val status = findClosestMessageStatus(
- timestamp = messageTimestamp,
- statusMap = pointerRefs,
- fallback = if (contents.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown
- )
-
- ChatItem.Message(
- chatMessageId = message.id,
- message = contents,
- date = message.dateMillis.toInstantFromMillis(),
- status = status,
- isFromSelf = contents.isFromSelf,
- key = contents.hashCode() + message.id.hashCode()
- )
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- conversationController.closeChatStream()
- }
-
- internal companion object {
- val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
- Timber.d("event=${event}")
- when (event) {
- is Event.OnConversationChanged -> { state ->
- val (conversation, _) = event.conversationWithPointers
- val members = conversation.nonSelfMembers
-
- state.copy(
- conversationId = conversation.id,
- identityRevealed = conversation.hasRevealedIdentity,
- pointers = event.conversationWithPointers.pointers,
- twitterUser = null,
- users = members.map {
- State.User(
- memberId = it.id,
- username = it.identity?.username,
- imageUrl = null,
- )
- }
- )
- }
-
- is Event.OnTipsChatCashChanged -> { state ->
- state.copy(
- tipChatCash = event.module
- )
- }
-
- is Event.OnCostToChatChanged -> { state ->
- state.copy(costToChat = event.cost)
- }
-
- is Event.OnMembersChanged -> { state ->
- state.copy(users = event.members)
- }
-
- is Event.OnPointersUpdated -> { state ->
- state.copy(pointers = event.pointers)
- }
-
- is Event.OnIdentityAvailable -> { state ->
- state.copy(identityAvailable = event.available)
- }
-
- is Event.OnTwitterUserChanged -> { state ->
- state.copy(twitterUser = event.user)
- }
-
- is Event.OnTypingStarted -> { state ->
- state.copy(showTypingIndicator = true)
- }
- is Event.OnTypingStopped -> { state ->
- state.copy(showTypingIndicator = false)
- }
-
- is Event.OnUserTypingStarted -> { state ->
- state.copy(isSelfTyping = true)
- }
- is Event.OnUserTypingStopped -> { state ->
- state.copy(isSelfTyping = false)
- }
-
- is Event.PresentPaymentConfirmation,
- is Event.OnChatIdChanged,
- is Event.Error,
- Event.RevealIdentity,
- Event.SendCash,
- is Event.MarkRead,
- is Event.MarkDelivered,
- is Event.SendMessage -> { state -> state }
-
- is Event.OnIdentityRevealed -> { state ->
- state.copy(identityRevealed = true)
- }
-
- is Event.OnUserRevealed -> { state ->
- val users = state.users
- val updatedUsers = users.map {
- if (it.memberId == event.memberId) {
- it.copy(
- username = event.username ?: it.username,
- imageUrl = event.imageUrl ?: it.imageUrl,
- )
- } else {
- it
- }
- }
-
- state.copy(users = updatedUsers)
- }
-
- is Event.OnUserActivity -> { state ->
- state.copy(lastSeen = event.activity)
- }
- }
- }
- }
-}
-
-private fun findClosestMessageStatus(
- timestamp: Long?,
- statusMap: Map,
- fallback: MessageStatus
-): MessageStatus {
- timestamp ?: return fallback
- var closestKey: Long? = null
-
- for (key in statusMap.keys) {
- if (timestamp <= key && (closestKey == null || key <= closestKey)) {
- closestKey = key
- }
- }
-
- return closestKey?.let { statusMap[it] } ?: fallback
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt
deleted file mode 100644
index 693f474a6..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameScreen.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.getcode.view.main.chat.create.byusername
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.getcode.R
-import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.navigation.screens.ConversationScreen
-import com.getcode.theme.CodeTheme
-import com.getcode.theme.inputColors
-import com.getcode.ui.components.ConstraintMode
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeScaffold
-import com.getcode.ui.components.TextInput
-import com.getcode.ui.components.keyboardAsState
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun ChatByUsernameScreen(
- viewModel: ChatByUsernameViewModel
-) {
- val state by viewModel.stateFlow.collectAsState()
- val navigator = LocalCodeNavigator.current
-
- val keyboardVisible by keyboardAsState()
- val keyboardController = LocalSoftwareKeyboardController.current
- val composeScope = rememberCoroutineScope()
- var isChecking by remember(state.checkingUsername) { mutableStateOf(false) }
-
- val checkUsername = {
- composeScope.launch {
- isChecking = true
- if (keyboardVisible) {
- keyboardController?.hide()
- delay(500)
- }
- viewModel.dispatchEvent(ChatByUsernameViewModel.Event.CheckUsername)
- }
- }
-
- LaunchedEffect(viewModel) {
- viewModel.eventFlow
- .filterIsInstance()
- .map { it.user }
- .onEach {
- navigator.push(ConversationScreen(user = it))
- }.launchIn(this)
- }
-
- CodeScaffold(
- modifier = Modifier.fillMaxSize().imePadding(),
- bottomBar = {
- Box(modifier = Modifier.fillMaxWidth()) {
- CodeButton(
- enabled = state.canAdvance,
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = CodeTheme.dimens.inset)
- .padding(bottom = CodeTheme.dimens.grid.x2),
- buttonState = ButtonState.Filled,
- text = stringResource(R.string.action_next),
- isLoading = isChecking,
- isSuccess = state.isValidUsername
- ) {
- checkUsername()
- }
- }
- }
- ) { padding ->
- val focusRequester = remember { FocusRequester() }
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding)
- ) {
- TextInput(
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.Center)
- .padding(CodeTheme.dimens.inset)
- .focusRequester(focusRequester),
- state = state.textFieldState,
- colors = inputColors(
- backgroundColor = Color.Transparent,
- borderColor = Color.Transparent
- ),
- maxLines = 1,
- constraintMode = ConstraintMode.AutoSize(minimum = CodeTheme.typography.displaySmall),
- contentPadding = PaddingValues(horizontal = 20.dp),
- style = CodeTheme.typography.displayMedium,
- placeholderStyle = CodeTheme.typography.displayMedium,
- placeholder = stringResource(R.string.subtitle_xUsername),
- )
- }
-
- LaunchedEffect(focusRequester) {
- focusRequester.requestFocus()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt b/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt
deleted file mode 100644
index 1dc263c38..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/create/byusername/ChatByUsernameViewModel.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-@file:OptIn(ExperimentalFoundationApi::class)
-
-package com.getcode.view.main.chat.create.byusername
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.text2.input.TextFieldState
-import androidx.compose.foundation.text2.input.clearText
-import androidx.lifecycle.viewModelScope
-import com.getcode.R
-import com.getcode.manager.TopBarManager
-import com.getcode.model.TwitterUser
-import com.getcode.network.TipController
-import com.getcode.network.TwitterUserController
-import com.getcode.util.resources.ResourceHelper
-import com.getcode.view.BaseViewModel2
-import com.getcode.view.main.chat.conversation.ConversationViewModel.Event
-import com.getcode.view.main.chat.conversation.ConversationViewModel.State
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import javax.inject.Inject
-
-@HiltViewModel
-class ChatByUsernameViewModel @Inject constructor(
- resources: ResourceHelper,
- twitterUserController: TwitterUserController,
-): BaseViewModel2(
- initialState = State(),
- updateStateForEvent = updateStateForEvent
-) {
- data class State(
- val checkingUsername: Boolean = false,
- val isValidUsername: Boolean = false,
- val textFieldState: TextFieldState = TextFieldState(),
- ) {
- val canAdvance: Boolean
- get() = textFieldState.text.isNotEmpty()
- }
-
- sealed interface Event {
- data object CheckUsername : Event
- data class OnSuccess(val user: TwitterUser) : Event
- data object OnError : Event
- }
-
- init {
- eventFlow
- .filterIsInstance()
- .map { stateFlow.value }
- .map {
- val textFieldState = it.textFieldState
- val text = textFieldState.text.toString()
-
- runCatching { twitterUserController.fetchUser(text) }
- }
- .map { it.getOrNull() }
- .onEach { twitterUser ->
- if (twitterUser == null) {
- dispatchEvent(Event.OnError)
- } else {
- dispatchEvent(Event.OnSuccess(twitterUser))
- }
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .onEach {
- TopBarManager.showMessage(
- TopBarManager.TopBarMessage(
- title = resources.getString(R.string.error_title_usernameNotFound),
- message = resources.getString(R.string.error_description_usernameNotFound)
- )
- )
- }.launchIn(viewModelScope)
- }
-
- internal companion object {
- val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
- when (event) {
- Event.CheckUsername -> { state ->
- state.copy(checkingUsername = true)
- }
- Event.OnError -> { state ->
- state.copy(checkingUsername = false)
- }
- is Event.OnSuccess -> { state ->
- state.copy(checkingUsername = false, isValidUsername = true)
- }
- }
- }
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index 9e722d56c..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/list/ChatListScreen.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.getcode.view.main.chat.list
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Divider
-import androidx.compose.material.Text
-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.Alignment.Companion.CenterHorizontally
-import androidx.compose.ui.Alignment.Companion.CenterVertically
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import com.getcode.R
-import com.getcode.navigation.core.LocalCodeNavigator
-import com.getcode.navigation.screens.ChatByUsernameScreen
-import com.getcode.navigation.screens.ConversationScreen
-import com.getcode.theme.CodeTheme
-import com.getcode.theme.White10
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeCircularProgressIndicator
-import com.getcode.ui.components.CodeScaffold
-import com.getcode.ui.components.chat.ChatNode
-
-@Composable
-fun ChatListScreen(
- viewModel: ChatListViewModel,
-) {
- val state by viewModel.stateFlow.collectAsState()
- val navigator = LocalCodeNavigator.current
-
- val chatsEmpty by remember(state.conversations) {
- derivedStateOf { state.conversations.isEmpty() }
- }
-
- CodeScaffold(
- bottomBar = {
- Box(modifier = Modifier.fillMaxWidth()) {
- CodeButton(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = CodeTheme.dimens.inset),
- buttonState = ButtonState.Filled,
- text = stringResource(R.string.action_startNewChat)
- ) {
- navigator.push(ChatByUsernameScreen)
- }
- }
- }
- ) { padding ->
- LazyColumn(modifier = Modifier.padding(padding)) {
- items(state.conversations, key = { it.id }) { chat ->
- ChatNode(chat = chat, showAvatar = true) {
- navigator.push(ConversationScreen(chatId = chat.id))
- }
- Divider(
- modifier = Modifier.padding(start = CodeTheme.dimens.inset),
- color = White10,
- )
- }
-
- when {
- state.loading -> {
- item {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(
- CodeTheme.dimens.grid.x2,
- CenterVertically
- ),
- ) {
- CodeCircularProgressIndicator()
- Text(
- modifier = Modifier.fillMaxWidth(0.6f),
- text = stringResource(R.string.subtitle_loadingChats),
- textAlign = TextAlign.Center
- )
- }
- }
- }
- chatsEmpty -> {
- item {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(
- CodeTheme.dimens.grid.x2,
- CenterVertically
- ),
- ) {
- Text(
- modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1),
- text = stringResource(R.string.subtitle_dontHaveChats),
- color = CodeTheme.colors.textSecondary,
- style = CodeTheme.typography.textMedium
- )
- }
- }
- }
- }
- }
- }
-}
\ 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
deleted file mode 100644
index f2f236378..000000000
--- a/app/src/main/java/com/getcode/view/main/chat/list/ChatListViewModel.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.getcode.view.main.chat.list
-
-import androidx.lifecycle.viewModelScope
-import com.getcode.model.chat.ConversationEntity
-import com.getcode.network.ConversationListController
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.view.BaseViewModel2
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import javax.inject.Inject
-
-@HiltViewModel
-class ChatListViewModel @Inject constructor(
- conversationsController: ConversationListController,
- networkObserver: NetworkConnectivityListener,
-): BaseViewModel2(
- initialState = State(),
- updateStateForEvent = updateStateForEvent
-) {
- data class State(
- val loading: Boolean = false,
- val conversations: List = emptyList(),
- )
-
- sealed interface Event {
- data class OnChatsLoading(val loading: Boolean) : Event
- data class OnChatsUpdated(val chats: List) : Event
- data object OnOpened: Event
- }
-
- init {
- conversationsController.observeConversations()
- .onEach {
- if (it == null || (it.isEmpty() && !networkObserver.isConnected)) {
- dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(true))
- }
- }
- .map { conversations ->
- when {
- conversations == null -> null // await for confirmation it's empty
- conversations.isEmpty() && !networkObserver.isConnected -> null // remain loading while disconnected
- conversationsController.isLoading -> null // remain loading while fetching messages
- else -> conversations
- }
- }
- .filterNotNull()
- .onEach { update ->
- dispatchEvent(Dispatchers.Main, Event.OnChatsUpdated(update))
- }.onEach {
- dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(false))
- }.launchIn(viewModelScope)
-
- eventFlow
- .filterIsInstance()
- .onEach { conversationsController.fetchChats() }
- .launchIn(viewModelScope)
- }
-
- companion object {
- val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
- when (event) {
- is Event.OnOpened -> { state -> state }
- is Event.OnChatsLoading -> { state -> state.copy(loading = event.loading) }
- is Event.OnChatsUpdated -> { state -> state.copy(conversations = event.chats) }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/connectivity/ConnectionStatusProvider.kt b/app/src/main/java/com/getcode/view/main/connectivity/ConnectionStatusProvider.kt
deleted file mode 100644
index 1e261f554..000000000
--- a/app/src/main/java/com/getcode/view/main/connectivity/ConnectionStatusProvider.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.getcode.view.main.connectivity
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import com.getcode.utils.network.ConnectionType
-import com.getcode.utils.network.NetworkState
-import com.getcode.utils.network.SignalStrength
-
-internal class NetworkStateProvider (
- override val values: Sequence = sequenceOf(
- NetworkState(connected = false, type = ConnectionType.Unknown, signalStrength = SignalStrength.Unknown),
- NetworkState(connected = false, type = ConnectionType.Wifi, signalStrength = SignalStrength.Great),
- NetworkState(connected = true, type = ConnectionType.Wifi, signalStrength = SignalStrength.Great),
- )
-): PreviewParameterProvider
\ No newline at end of file
diff --git a/app/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt b/app/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt
index 949068af3..106e8595f 100644
--- a/app/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt
+++ b/app/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt
@@ -57,10 +57,9 @@ import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.Brand
import com.getcode.theme.CodeTheme
-import com.getcode.theme.White05
import com.getcode.theme.White50
import com.getcode.theme.inputColors
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.utils.keyboardAsState
import com.getcode.ui.utils.rememberedClickable
import com.getcode.view.main.giveKin.CurrencyListItem
@@ -331,7 +330,7 @@ private fun GroupHeader(modifier: Modifier = Modifier, text: String) {
)
}
Divider(
- color = White05,
+ color = CodeTheme.colors.dividerVariant,
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
@@ -433,7 +432,7 @@ private fun ListRowItem(
}
Divider(
- color = White05,
+ color = CodeTheme.colors.dividerVariant,
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
diff --git a/app/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt b/app/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt
index 5b4427a24..f10e0798d 100644
--- a/app/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt
@@ -4,12 +4,10 @@ import androidx.compose.runtime.Stable
import androidx.lifecycle.viewModelScope
import com.getcode.R
import com.getcode.model.Currency
-import com.getcode.model.PrefString
-import com.getcode.model.PrefsBool
-import com.getcode.model.PrefsString
+import com.getcode.services.model.PrefsBool
+import com.getcode.services.model.PrefsString
import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.PrefRepository
-import com.getcode.util.CurrencyUtils
import com.getcode.util.locale.LocaleHelper
import com.getcode.util.resources.ResourceHelper
import com.getcode.view.BaseViewModel2
@@ -28,7 +26,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
-import kotlinx.serialization.Serializable
import javax.inject.Inject
enum class CurrencySelectKind {
@@ -45,7 +42,7 @@ enum class CurrencySelectKind {
@HiltViewModel
class CurrencyViewModel @Inject constructor(
localeHelper: LocaleHelper,
- currencyUtils: CurrencyUtils,
+ currencyUtils: com.getcode.utils.CurrencyUtils,
exchange: Exchange,
private val prefsRepository: PrefRepository,
private val resources: ResourceHelper,
diff --git a/app/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt b/app/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt
index 7ef34e2b7..5df497b2c 100644
--- a/app/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt
+++ b/app/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt
@@ -29,8 +29,8 @@ import com.getcode.R
import com.getcode.theme.CodeTheme
import com.getcode.theme.bolded
import com.getcode.ui.utils.rememberedClickable
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
diff --git a/app/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt b/app/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt
index c7b6409b9..a1c6aca26 100644
--- a/app/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt
@@ -20,18 +20,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.getcode.LocalBetaFlags
-import com.getcode.LocalNetworkObserver
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.KadoWebScreen
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeKeyPad
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeKeyPad
import com.getcode.ui.components.Row
import com.getcode.util.showNetworkError
import com.getcode.utils.ErrorUtils
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.utils.network.LocalNetworkObserver
+import com.getcode.ui.components.text.AmountArea
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
diff --git a/app/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt b/app/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt
index 31697c0e6..06f64dfdc 100644
--- a/app/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt
@@ -12,6 +12,7 @@ import com.getcode.model.CurrencyCode
import com.getcode.model.Fiat
import com.getcode.model.KinAmount
import com.getcode.model.Rate
+import com.getcode.model.fromFiatAmount
import com.getcode.network.client.Client
import com.getcode.network.client.declareFiatPurchase
import com.getcode.network.client.linkAdditionalAccount
@@ -20,15 +21,12 @@ import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PhoneRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
+import com.getcode.services.utils.makeE164
import com.getcode.solana.organizer.AccountType
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.locale.LocaleHelper
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.FormatUtils
import com.getcode.utils.blockchainMemo
-import com.getcode.utils.makeE164
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.view.main.giveKin.AmountAnimatedInputUiModel
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import com.getcode.view.main.giveKin.AmountUiModel
import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel
import com.getcode.view.main.giveKin.CurrencyUiModel
@@ -52,9 +50,9 @@ class BuyKinViewModel @Inject constructor(
prefsRepository: PrefRepository,
balanceRepository: BalanceRepository,
transactionRepository: TransactionRepository,
- localeHelper: LocaleHelper,
- private val currencyUtils: CurrencyUtils,
- private val networkObserver: NetworkConnectivityListener,
+ localeHelper: com.getcode.util.locale.LocaleHelper,
+ private val currencyUtils: com.getcode.utils.CurrencyUtils,
+ private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
resources: ResourceHelper,
private val phoneRepository: PhoneRepository,
) : BaseAmountCurrencyViewModel(
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 6954fbe7e..198e95a1e 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
@@ -52,8 +52,8 @@ import com.getcode.theme.Success
import com.getcode.theme.White
import com.getcode.theme.White05
import com.getcode.theme.bolded
-import com.getcode.ui.components.CodeCircularProgressIndicator
-import com.getcode.ui.components.CodeScaffold
+import com.getcode.ui.theme.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeScaffold
import com.getcode.ui.components.showSnackbar
import com.getcode.ui.utils.addIf
import com.getcode.ui.utils.rememberedClickable
@@ -263,7 +263,7 @@ private fun GetKinItemRow(modifier: Modifier = Modifier, item: GetKinItem) {
) {
Text(
text = item.titleText,
- color = if (item.isActive) Color.White else BrandLight,
+ color = if (item.isActive) Color.White else CodeTheme.colors.brandLight,
style = CodeTheme.typography.textSmall.copy(
textDecoration = if (item.isStrikeThrough) TextDecoration.LineThrough else null,
),
diff --git a/app/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt b/app/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt
index 725d4d971..7bbfbe684 100644
--- a/app/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt
@@ -18,7 +18,7 @@ import androidx.compose.ui.graphics.Color
import com.getcode.manager.TopBarManager
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.KadoWebScreen.BuyKinWebInterface
-import com.getcode.ui.components.CodeCircularProgressIndicator
+import com.getcode.ui.theme.CodeCircularProgressIndicator
import com.getcode.ui.utils.toAGColor
import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.LoadingState
@@ -45,7 +45,6 @@ fun BoxScope.KadoWebScreen(
object : AccompanistWebViewClient() {
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
- println("url=$url")
if (url?.startsWith("https://app.kado.money/ramp/order/") == true) {
// order created, extract order id
orderId = Uri.parse(url).lastPathSegment.orEmpty()
diff --git a/app/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt b/app/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt
index fe6a8ce01..0f54cdf44 100644
--- a/app/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt
@@ -11,14 +11,13 @@ import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
-import com.getcode.network.repository.replaceParam
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.Kin
-import com.getcode.util.NumberInputHelper
-import com.getcode.util.locale.LocaleHelper
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
+import com.getcode.ui.components.text.AmountInputViewModel
+import com.getcode.utils.Kin
+import com.getcode.ui.components.text.NumberInputHelper
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.FormatUtils
-import com.getcode.utils.network.NetworkConnectivityListener
+import com.getcode.utils.replaceParam
import com.getcode.view.BaseViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -88,10 +87,10 @@ abstract class BaseAmountCurrencyViewModel(
protected val exchange: Exchange,
private val balanceRepository: BalanceRepository,
private val transactionRepository: TransactionRepository,
- protected val localeHelper: LocaleHelper,
- private val currencyUtils: CurrencyUtils,
+ protected val localeHelper: com.getcode.util.locale.LocaleHelper,
+ private val currencyUtils: com.getcode.utils.CurrencyUtils,
protected val resources: ResourceHelper,
- private val networkObserver: NetworkConnectivityListener,
+ private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
) : BaseViewModel(resources), AmountInputViewModel {
protected val numberInputHelper = NumberInputHelper()
abstract fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel)
diff --git a/app/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt b/app/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt
index c17591a16..291c4cf38 100644
--- a/app/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt
@@ -18,20 +18,19 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import cafe.adriel.voyager.navigator.currentOrThrow
-import com.getcode.LocalNetworkObserver
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.models.Bill
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.CurrencySelectionModal
-import com.getcode.theme.Alert
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeKeyPad
+import com.getcode.ui.components.text.AmountArea
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeKeyPad
import com.getcode.util.showNetworkError
import com.getcode.utils.ErrorUtils
+import com.getcode.utils.network.LocalNetworkObserver
import kotlinx.coroutines.launch
@Preview
@@ -62,7 +61,7 @@ fun GiveKinScreen(
}
val color =
- if (isInError) Alert else BrandLight
+ if (isInError) CodeTheme.colors.errorText else CodeTheme.colors.brandLight
Box(
modifier = Modifier.weight(0.65f)
) {
diff --git a/app/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt b/app/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt
index dbb722ef1..2f35b8e2c 100644
--- a/app/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt
@@ -5,18 +5,17 @@ import com.getcode.R
import com.getcode.manager.TopBarManager
import com.getcode.model.CurrencyCode
import com.getcode.model.KinAmount
+import com.getcode.model.fromFiatAmount
import com.getcode.network.client.Client
import com.getcode.network.client.receiveIfNeeded
import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
-import com.getcode.network.repository.replaceParam
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.locale.LocaleHelper
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
-import com.getcode.utils.network.NetworkConnectivityListener
+import com.getcode.utils.replaceParam
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -39,9 +38,9 @@ class GiveKinSheetViewModel @Inject constructor(
prefsRepository: PrefRepository,
balanceRepository: BalanceRepository,
transactionRepository: TransactionRepository,
- localeHelper: LocaleHelper,
- currencyUtils: CurrencyUtils,
- networkObserver: NetworkConnectivityListener,
+ localeHelper: com.getcode.util.locale.LocaleHelper,
+ currencyUtils: com.getcode.utils.CurrencyUtils,
+ networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
resources: ResourceHelper,
) : BaseAmountCurrencyViewModel(
client,
diff --git a/app/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt b/app/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt
index cf33c0f25..a8a68d22b 100644
--- a/app/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt
@@ -16,20 +16,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import cafe.adriel.voyager.navigator.currentOrThrow
-import com.getcode.LocalNetworkObserver
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.CurrencySelectionModal
-import com.getcode.theme.Alert
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeKeyPad
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeKeyPad
import com.getcode.util.showNetworkError
import com.getcode.utils.ErrorUtils
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.utils.network.LocalNetworkObserver
+import com.getcode.ui.components.text.AmountArea
import kotlinx.coroutines.launch
@Preview
@@ -53,7 +51,7 @@ fun RequestKinScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
val color =
- if (dataState.amountModel.amountKin > dataState.amountModel.buyLimitKin) Alert else BrandLight
+ if (dataState.amountModel.amountKin > dataState.amountModel.buyLimitKin) CodeTheme.colors.errorText else CodeTheme.colors.brandLight
Box(
modifier = Modifier.weight(0.65f)
) {
diff --git a/app/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt b/app/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt
index 8d3ab8ead..9ddd984e6 100644
--- a/app/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt
@@ -3,18 +3,16 @@ package com.getcode.view.main.requestKin
import androidx.lifecycle.viewModelScope
import com.getcode.model.CurrencyCode
import com.getcode.model.KinAmount
+import com.getcode.model.fromFiatAmount
import com.getcode.network.client.Client
import com.getcode.network.client.receiveIfNeeded
import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.locale.LocaleHelper
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.view.main.giveKin.AmountAnimatedInputUiModel
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import com.getcode.view.main.giveKin.AmountUiModel
import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel
import com.getcode.view.main.giveKin.CurrencyUiModel
@@ -33,9 +31,9 @@ class RequestKinViewModel @Inject constructor(
prefsRepository: PrefRepository,
balanceRepository: BalanceRepository,
transactionRepository: TransactionRepository,
- localeHelper: LocaleHelper,
- currencyUtils: CurrencyUtils,
- networkObserver: NetworkConnectivityListener,
+ localeHelper: com.getcode.util.locale.LocaleHelper,
+ currencyUtils: com.getcode.utils.CurrencyUtils,
+ networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
resources: ResourceHelper,
) : BaseAmountCurrencyViewModel(
client,
diff --git a/app/src/main/java/com/getcode/view/main/scanner/DecorView.kt b/app/src/main/java/com/getcode/view/main/scanner/DecorView.kt
index f03519ab4..179ac583c 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/DecorView.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/DecorView.kt
@@ -40,13 +40,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.getcode.SessionState
-import com.getcode.LocalNetworkObserver
import com.getcode.R
import com.getcode.theme.CodeTheme
import com.getcode.theme.xxl
import com.getcode.ui.components.Pill
import com.getcode.ui.tips.DefinedTips
import com.getcode.ui.utils.unboundedClickable
+import com.getcode.utils.network.LocalNetworkObserver
import com.getcode.view.main.scanner.components.HomeBottom
import dev.bmcreations.tipkit.LocalTipProvider
import dev.bmcreations.tipkit.engines.LocalTipsEngine
diff --git a/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt b/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt
index ee03a0244..ba6d7e409 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt
@@ -46,7 +46,6 @@ import com.getcode.SessionController
import com.getcode.LocalBiometricsState
import com.getcode.PresentationStyle
import com.getcode.R
-import com.getcode.RestrictionType
import com.getcode.manager.TopBarManager
import com.getcode.models.Bill
import com.getcode.models.DeepLinkRequest
@@ -54,27 +53,26 @@ 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.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
import com.getcode.navigation.screens.ShareDownloadLinkModal
import com.getcode.ui.components.OnLifecycleEvent
-import com.getcode.ui.components.PermissionResult
-import com.getcode.ui.components.getPermissionLauncher
-import com.getcode.ui.components.rememberPermissionChecker
import com.getcode.ui.utils.AnimationUtils
import com.getcode.ui.utils.measured
import com.getcode.util.launchAppSettings
-import com.getcode.view.login.notificationPermissionCheck
import com.getcode.view.main.bill.BillManagementOptions
import com.getcode.view.main.scanner.views.CameraDisabledView
import com.getcode.view.main.scanner.camera.CodeScanner
import com.getcode.view.main.bill.HomeBill
import com.getcode.view.main.scanner.views.CameraPermissionsMissingView
import com.getcode.ui.modals.ReceivedKinConfirmation
-import com.getcode.view.main.scanner.views.HomeRestricted
+import com.getcode.util.permissions.PermissionResult
+import com.getcode.util.permissions.getPermissionLauncher
+import com.getcode.util.permissions.rememberPermissionHandler
+import com.getcode.ui.components.restrictions.ContentRestrictedView
+import com.getcode.ui.components.restrictions.RestrictionType
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -91,7 +89,6 @@ enum class UiElement {
BALANCE,
SHARE_DOWNLOAD,
TIP_CARD,
- CHAT,
GALLERY
}
@@ -108,7 +105,7 @@ fun ScanScreen(
RestrictionType.ACCESS_EXPIRED,
RestrictionType.FORCE_UPGRADE,
RestrictionType.TIMELOCK_UNLOCKED -> {
- HomeRestricted(restrictionType) {
+ ContentRestrictedView(restrictionType) {
session.logout(it)
}
}
@@ -121,7 +118,8 @@ fun ScanScreen(
request = request,
)
- val notificationPermissionChecker = notificationPermissionCheck { }
+ val notificationPermissionChecker =
+ com.getcode.util.permissions.notificationPermissionCheck { }
val context = LocalContext.current
LaunchedEffect(session) {
session.eventFlow
@@ -231,7 +229,6 @@ private fun ScannerContent(
}
}
- UiElement.CHAT -> navigator.show(ChatListModal)
UiElement.GALLERY -> {
pickPhoto.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
@@ -348,7 +345,7 @@ private fun BillContainer(
val cameraPermissionLauncher = getPermissionLauncher(Manifest.permission.CAMERA, onPermissionResult)
- val permissionChecker = rememberPermissionChecker()
+ val permissionChecker = rememberPermissionHandler()
val checkPermission = { shouldRequest: Boolean ->
permissionChecker.request(
diff --git a/app/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt b/app/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt
index a3bdb8359..393a3b0d3 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt
@@ -29,7 +29,6 @@ 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.unboundedClickable
import com.getcode.ui.utils.widthOrZero
@@ -92,16 +91,6 @@ internal fun HomeBottom(
)
}
- UiElement.CHAT -> {
- BottomBarAction(
- modifier = Modifier.weight(1f),
- label = stringResource(R.string.action_chat),
- painter = painterResource(R.drawable.ic_chat),
- badgeCount = state.chatUnreadCount,
- onClick = { onPress(action) },
- )
- }
-
else -> {
BottomBarAction(
modifier = Modifier
@@ -140,7 +129,7 @@ private fun BottomBarAction(
Badge(
modifier = Modifier.padding(top = 6.dp, end = 1.dp),
count = badgeCount,
- color = ChatNodeDefaults.UnreadIndicator,
+ color = CodeTheme.colors.indicator,
enterTransition = scaleIn(
animationSpec = tween(
durationMillis = 300,
diff --git a/app/src/main/java/com/getcode/view/main/scanner/views/BiometricsBlockingView.kt b/app/src/main/java/com/getcode/view/main/scanner/views/BiometricsBlockingView.kt
index fce802afd..4157fa67a 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/views/BiometricsBlockingView.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/views/BiometricsBlockingView.kt
@@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.getcode.R
import com.getcode.theme.Brand
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
import com.getcode.ui.utils.BiometricsState
@Composable
diff --git a/app/src/main/java/com/getcode/view/main/scanner/views/CameraDisabledView.kt b/app/src/main/java/com/getcode/view/main/scanner/views/CameraDisabledView.kt
index d22fcccd3..4cdf6aabf 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/views/CameraDisabledView.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/views/CameraDisabledView.kt
@@ -11,14 +11,13 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.getcode.R
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@Composable
internal fun CameraDisabledView(
diff --git a/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt b/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt
index 394a2f130..6112fecdc 100644
--- a/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt
+++ b/app/src/main/java/com/getcode/view/main/scanner/views/CameraPermissionsMissingView.kt
@@ -17,8 +17,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.getcode.R
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
@Composable
internal fun CameraPermissionsMissingView(
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 58b51c862..72951ee0e 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
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -29,6 +28,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.getcode.R
@@ -36,12 +36,13 @@ import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.theme.Brand
import com.getcode.theme.BrandSubtle
import com.getcode.theme.CodeTheme
+import com.getcode.theme.DesignSystem
import com.getcode.theme.bolded
import com.getcode.theme.extraSmall
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeScaffold
import com.getcode.ui.components.Row
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
@@ -50,11 +51,13 @@ import kotlinx.coroutines.flow.onEach
enum class IdentityConnectionReason {
TipCard,
IdentityReveal,
+ Login,
}
@Composable
fun ConnectAccountScreen(
- viewModel: TipConnectViewModel = hiltViewModel()
+ viewModel: ConnectAccountViewModel = hiltViewModel(),
+ titleAlignment: TextAlign = TextAlign.Start,
) {
val state by viewModel.stateFlow.collectAsState()
val navigator = LocalCodeNavigator.current
@@ -62,11 +65,12 @@ fun ConnectAccountScreen(
LaunchedEffect(viewModel) {
viewModel.eventFlow
- .filterIsInstance()
+ .filterIsInstance()
.onEach {
composeTweet(context, it.intent)
delay(1_000)
when (state.reason) {
+ IdentityConnectionReason.Login,
IdentityConnectionReason.IdentityReveal -> navigator.pop()
else -> navigator.hide()
}
@@ -80,7 +84,7 @@ fun ConnectAccountScreen(
modifier = Modifier.fillMaxWidth()
.padding(CodeTheme.dimens.inset),
onClick = {
- viewModel.dispatchEvent(TipConnectViewModel.Event.PostToX)
+ viewModel.dispatchEvent(ConnectAccountViewModel.Event.PostToX)
},
buttonState = ButtonState.Filled,
content = {
@@ -103,29 +107,35 @@ fun ConnectAccountScreen(
.padding(padding),
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset)
) {
- RequestContent(state = state)
+ RequestContent(state = state, titleAlignment = titleAlignment)
}
}
}
@Composable
-private fun ColumnScope.RequestContent(state: TipConnectViewModel.State) {
+private fun ColumnScope.RequestContent(state: ConnectAccountViewModel.State, titleAlignment: TextAlign = TextAlign.Start) {
Text(
+ modifier = Modifier.fillMaxWidth(),
text = when(state.reason) {
IdentityConnectionReason.TipCard -> stringResource(id = R.string.title_receiveTips)
IdentityConnectionReason.IdentityReveal -> stringResource(id = R.string.title_connectAccount)
+ IdentityConnectionReason.Login -> stringResource(id = R.string.title_connectYourX)
null -> ""
},
- style = CodeTheme.typography.displayMedium.bolded()
+ style = CodeTheme.typography.displayMedium.bolded(),
+ textAlign = titleAlignment,
)
Text(
+ modifier = Modifier.fillMaxWidth(),
text = when(state.reason) {
IdentityConnectionReason.TipCard -> stringResource(id = R.string.subtitle_tipCardXDescription)
IdentityConnectionReason.IdentityReveal -> stringResource(id = R.string.subtitle_connectXAccount)
+ IdentityConnectionReason.Login -> stringResource(id = R.string.subtitle_identityInApp, stringResource(R.string.app_name_without_variant))
null -> ""
},
- style = CodeTheme.typography.textSmall
+ style = CodeTheme.typography.textSmall,
+ textAlign = titleAlignment,
)
Spacer(modifier = Modifier.weight(0.3f))
TweetPreview(modifier = Modifier.fillMaxWidth(), xMessage = state.xMessage)
@@ -167,7 +177,7 @@ private fun composeTweet(context: Context, intent: Intent) {
@Preview
@Composable
private fun Preview_TweetPreview() {
- CodeTheme {
+ DesignSystem {
TweetPreview(xMessage = "${stringResource(R.string.subtitle_connectXTweetText)}\n" +
"\n" +
"CodeAccount:349pQtzGmiBxU9vADVf6AUdMLLXyCCU3Zu4smrQPXved:zGmiBxU9vADVf6AUdMLLXyCCU3Zu4smrQP")
diff --git a/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt b/app/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt
similarity index 73%
rename from app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt
rename to app/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt
index 9704ac1cf..857d87a74 100644
--- a/app/src/main/java/com/getcode/view/main/tip/TipConnectViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.getcode.R
import com.getcode.analytics.Action
import com.getcode.analytics.AnalyticsService
+import com.getcode.network.IdentityManager
import com.getcode.network.TipController
import com.getcode.util.IntentUtils
import com.getcode.util.resources.ResourceHelper
@@ -18,11 +19,12 @@ import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@HiltViewModel
-class TipConnectViewModel @Inject constructor(
+class ConnectAccountViewModel @Inject constructor(
resources: ResourceHelper,
tipController: TipController,
analytics: AnalyticsService,
-) : BaseViewModel2(
+ identityManager: IdentityManager,
+) : BaseViewModel2(
initialState = State(null, ""),
updateStateForEvent = updateStateForEvent
) {
@@ -44,9 +46,10 @@ class TipConnectViewModel @Inject constructor(
.filterIsInstance()
.map { it.reason }
.mapNotNull {
- val verificationMessage = tipController.generateTipVerification() ?: return@mapNotNull null
when (it) {
IdentityConnectionReason.TipCard -> {
+ val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null
+
"""
${resources.getString(R.string.subtitle_connectXTweetText)}
@@ -55,12 +58,23 @@ class TipConnectViewModel @Inject constructor(
}
IdentityConnectionReason.IdentityReveal -> {
+ val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null
"""
${resources.getString(R.string.subtitle_linkingTwitterToRevealIdentity)}
$verificationMessage
""".trimIndent()
}
+
+ IdentityConnectionReason.Login -> {
+ val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null
+
+ """
+ ${resources.getString(R.string.subtitle_linkingTwitterToLogin, resources.getString(R.string.handle))}
+
+ $verificationMessage
+ """.trimIndent()
+ }
}
}.onEach {
dispatchEvent(Event.UpdateMessage(it))
diff --git a/app/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt b/app/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt
index d8983ab28..a9b695216 100644
--- a/app/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt
+++ b/app/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt
@@ -17,20 +17,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import cafe.adriel.voyager.navigator.currentOrThrow
-import com.getcode.LocalNetworkObserver
import com.getcode.LocalSession
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.CurrencySelectionModal
import com.getcode.theme.Alert
-import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
-import com.getcode.ui.components.ButtonState
-import com.getcode.ui.components.CodeButton
-import com.getcode.ui.components.CodeKeyPad
+import com.getcode.ui.components.text.AmountArea
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.theme.CodeKeyPad
import com.getcode.util.showNetworkError
import com.getcode.utils.ErrorUtils
-import com.getcode.view.main.giveKin.AmountArea
+import com.getcode.utils.network.LocalNetworkObserver
import kotlinx.coroutines.launch
@Composable
@@ -61,7 +60,7 @@ fun EnterTipScreen(
}
}
- val color = if (isInError) Alert else BrandLight
+ val color = if (isInError) Alert else CodeTheme.colors.textSecondary
Box(
modifier = Modifier.weight(0.65f)
) {
diff --git a/app/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt b/app/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt
index 155a368e1..f45bc1e32 100644
--- a/app/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt
+++ b/app/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt
@@ -2,26 +2,24 @@ package com.getcode.view.main.tip
import androidx.lifecycle.viewModelScope
import com.getcode.R
-import com.getcode.manager.TopBarManager
import com.getcode.model.CurrencyCode
import com.getcode.model.Kin
import com.getcode.model.KinAmount
import com.getcode.model.Rate
import com.getcode.model.SendLimit
+import com.getcode.model.fromFiatAmount
import com.getcode.network.client.Client
import com.getcode.network.client.receiveIfNeeded
import com.getcode.network.exchange.Exchange
import com.getcode.network.repository.BalanceRepository
import com.getcode.network.repository.PrefRepository
import com.getcode.network.repository.TransactionRepository
-import com.getcode.util.CurrencyUtils
-import com.getcode.util.formattedRaw
-import com.getcode.util.locale.LocaleHelper
+import com.getcode.extensions.formattedRaw
+import com.getcode.manager.TopBarManager
import com.getcode.util.resources.ResourceHelper
import com.getcode.utils.ErrorUtils
import com.getcode.utils.FormatUtils
-import com.getcode.utils.network.NetworkConnectivityListener
-import com.getcode.view.main.giveKin.AmountAnimatedInputUiModel
+import com.getcode.ui.components.text.AmountAnimatedInputUiModel
import com.getcode.view.main.giveKin.AmountUiModel
import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel
import com.getcode.view.main.giveKin.CurrencyUiModel
@@ -40,9 +38,9 @@ class TipPaymentViewModel @Inject constructor(
prefsRepository: PrefRepository,
balanceRepository: BalanceRepository,
private val transactionRepository: TransactionRepository,
- localeHelper: LocaleHelper,
- currencyUtils: CurrencyUtils,
- networkObserver: NetworkConnectivityListener,
+ localeHelper: com.getcode.util.locale.LocaleHelper,
+ currencyUtils: com.getcode.utils.CurrencyUtils,
+ networkObserver: com.getcode.utils.network.NetworkConnectivityListener,
resources: ResourceHelper,
) : BaseAmountCurrencyViewModel(
client,
diff --git a/app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeAnalyzer.kt b/app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeAnalyzer.kt
index 7d2be0f6c..2120e57ac 100644
--- a/app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeAnalyzer.kt
+++ b/app/src/main/java/com/kik/kikx/kikcodes/implementation/KikCodeAnalyzer.kt
@@ -1,6 +1,5 @@
package com.kik.kikx.kikcodes.implementation
-import android.content.Context
import android.net.Uri
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
@@ -12,7 +11,6 @@ import com.getcode.utils.ErrorUtils
import com.kik.kikx.kikcodes.KikCodeScanner
import com.kik.kikx.kikcodes.ScannerError
import com.kik.kikx.models.ScannableKikCode
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
diff --git a/app/src/main/res/drawable-nodpi/ic_notification_request.png b/app/src/main/res/drawable/ic_notification_request.png
similarity index 100%
rename from app/src/main/res/drawable-nodpi/ic_notification_request.png
rename to app/src/main/res/drawable/ic_notification_request.png
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index bbd3e0212..7353dbd1f 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index bbd3e0212..7353dbd1f 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index aa1162753..1d56a948f 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -19,6 +19,7 @@
#0F0C1F
#7379A0
#565C86
+ #443091
#0F0C1F
#8785A9
diff --git a/app/src/main/res/values/strings-universal.xml b/app/src/main/res/values/strings-universal.xml
index 060be710b..c3bf55070 100644
--- a/app/src/main/res/values/strings-universal.xml
+++ b/app/src/main/res/values/strings-universal.xml
@@ -1,6 +1,8 @@
- Code
+ https://app.getcode.com/tos
+ https://app.getcode.com/privacy-policy
+
https://getcode.com/d
https://getcode.com/d?r=%1$s
aqr
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1073dd58a..db4f53c10 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,10 +1,11 @@
+ com.getcode.accountprovider
Nevermind
- This payment request could not be paid at this time. Please try again later.
This login request could not be completed at this time. Please try again later.
Failed to create a USDC deposit account.
Payment Failed
+ This payment request could not be paid at this time. Please try again later.
Login Failed
Account Error
You can only request up to %1$s
@@ -24,12 +25,6 @@
Message
Messaged
- You revealed your identity to %1$s
- %1$s revealed their identity to you
- This person tipped you %1$s
- 🙏 You thanked them
- 🙏 %1$s thanked you for your tip
-
Send Kin
Require Biometrics
@@ -62,4 +57,17 @@
Loading your chats
Start a New Chat
You don\'t have any chats yet.
+
+ Connect Your X
+ Identity in %1$s is based on your X identity.
+ %1$s I’d like to connect my X
+ Message %1$s to Connect
+ Cash
+ Chats
+ Settings
+
+ Code
+ Code
+ \@getcode
+ CodeAccount
diff --git a/app/src/release/AndroidManifest.xml b/app/src/release/AndroidManifest.xml
new file mode 100644
index 000000000..16c292d86
--- /dev/null
+++ b/app/src/release/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 96e44c946..89f3c5583 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -34,6 +34,10 @@ allprojects {
configurations.all {
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7")
}
+
+// tasks.matching { it.name.contains("kapt") }.configureEach {
+// enabled = false
+// }
}
tasks.register("clean", Delete::class) {
diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index e9bc91c69..9b88f8f6d 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -1,7 +1,10 @@
@file:Suppress("ConstPropertyName")
+
object Android {
- const val namespace = "com.getcode"
+ const val codeNamespace = "com.getcode"
+ const val flipchatNamespace = "xyz.flipchat"
+
const val compileSdkVersion = 34
const val minSdkVersion = 24
const val targetSdkVersion = 34
@@ -9,23 +12,36 @@ object Android {
const val buildToolsVersion = "34.0.0"
}
-object Packaging {
- private const val majorVersion = 2
- private const val minorVersion = 1
- private const val patchVersion = 14
-
- const val versionName = "$majorVersion.$minorVersion.$patchVersion"
+sealed class Packaging(
+ majorVersion: Int,
+ minorVersion: Int,
+ patchVersion: Int,
+) {
+ val versionName = "$majorVersion.$minorVersion.$patchVersion"
+
+ object Code: Packaging(
+ majorVersion = 2,
+ minorVersion = 1,
+ patchVersion = 14,
+ )
+
+ object Flipchat: Packaging(
+ majorVersion = 1,
+ minorVersion = 0,
+ patchVersion = 6,
+ )
}
object Versions {
const val java = "17"
- const val kotlin = "1.9.23"
+ const val kotlin = "1.9.25"
const val kotlinx_coroutines = "1.7.3"
const val kotlinx_serialization = "1.6.2"
const val kotlinx_datetime = "0.5.0"
- const val android_gradle_build_tools = "8.4.0"
+ const val android_gradle_build_tools = "8.6.0"
const val google_services = "4.3.15"
+ const val androidx_appcompat = "1.7.0"
const val androidx_activity = "1.7.2"
const val androidx_annotation = "1.7.1"
const val androidx_biometrics = "1.2.0-alpha05"
@@ -39,20 +55,20 @@ object Versions {
const val androidx_room = "2.6.1"
const val sqlcipher = "4.5.1@aar"
- const val compose = "2024.05.00"
+ const val compose = "2024.10.01"
// compose compiler is tied to [Versions.kotlin]
// See compatibility mapping here:
// https://developer.android.com/jetpack/androidx/releases/compose-compiler
- const val compose_compiler = "1.5.11"
+ const val compose_compiler = "1.5.15"
const val compose_activities: String = "1.8.2"
const val compose_view_models: String = "2.6.2"
const val compose_navigation: String = "2.7.3"
const val compose_paging = "3.3.0-alpha02"
const val compose_webview = "0.33.6"
- const val hilt = "2.50"
- const val hilt_jetpack = "1.1.0-beta01"
+ const val hilt = "2.52"
+ const val hilt_jetpack = "1.2.0"
const val okhttp = "4.9.3"
const val retrofit = "2.6.0"
const val rxjava: String = "3.1.3"
@@ -67,6 +83,7 @@ object Versions {
const val crashlytics_gradle: String = "2.8.1"
const val play_service_auth = "20.7.0"
const val play_service_auth_phone = "18.0.2"
+ const val google_play_billing = "7.1.1"
const val grpc: String = "1.62.2"
const val grpc_okhttp: String = "1.33.1"
@@ -76,7 +93,6 @@ object Versions {
const val mp_android_chart: String = "v3.1.0"
const val lib_phone_number_port: String = "8.12.43"
const val lib_phone_number_google: String = "8.12.54"
- const val hilt_nav_compose: String = "1.1.0-alpha02"
const val zxing: String = "3.3.2"
const val androidx_test_runner = "1.4.0"
@@ -87,7 +103,7 @@ object Versions {
const val markwon = "4.6.2"
const val timber = "5.0.1"
- const val voyager = "1.0.0"
+ const val voyager = "1.1.0-beta02"
const val protobuf_plugin = "0.9.4"
const val sodium_bindings = "0.9.0"
@@ -137,6 +153,7 @@ object Plugins {
object Libs {
const val android_desugaring = "com.android.tools:desugar_jdk_libs:${Versions.desugaring}"
+ const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
const val androidx_activity = "androidx.activity:activity-ktx:{${Versions.androidx_activity}"
const val androidx_annotation = "androidx.annotation:annotation:${Versions.androidx_annotation}"
const val androidx_biometrics = "androidx.biometric:biometric:${Versions.androidx_biometrics}"
@@ -211,6 +228,7 @@ 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_materialIconsCore = "androidx.compose.material:material-icons-core"
const val compose_materialIconsExtended =
"androidx.compose.material:material-icons-extended-android"
const val compose_activities =
@@ -227,6 +245,8 @@ object Libs {
"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_tabs =
+ "cafe.adriel.voyager:voyager-tab-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}"
@@ -251,6 +271,9 @@ object Libs {
const val play_service_auth_phone =
"com.google.android.gms:play-services-auth-api-phone:${Versions.play_service_auth_phone}"
+ const val google_play_billing_runtime = "com.android.billingclient:billing:${Versions.google_play_billing}"
+ const val google_play_billing_ktx = "com.android.billingclient:billing-ktx:${Versions.google_play_billing}"
+
const val grpc_okhttp = "io.grpc:grpc-okhttp:${Versions.grpc_okhttp}"
const val grpc_kotlin = "io.grpc:grpc-kotlin-stub:${Versions.grpc_kotlin}"
const val grpc_protobuf = "io.grpc:grpc-protobuf:${Versions.grpc}"
@@ -266,7 +289,7 @@ object Libs {
"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 hilt_nav_compose = "androidx.hilt:hilt-navigation-compose:${Versions.hilt_jetpack}"
const val zxing = "com.google.zxing:core:${Versions.zxing}"
const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
@@ -298,5 +321,6 @@ object Libs {
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"
+ const val rinku = "dev.theolm:rinku:1.1.0"
+ const val rinku_compose = "dev.theolm:rinku-compose-ext:1.1.0"
}
diff --git a/crypto/ed25519/src/main/AndroidManifest.xml b/crypto/ed25519/src/main/AndroidManifest.xml
deleted file mode 100644
index 29188a871..000000000
--- a/crypto/ed25519/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/crypto/kin/.gitignore b/definitions/code-vm/models/.gitignore
similarity index 100%
rename from crypto/kin/.gitignore
rename to definitions/code-vm/models/.gitignore
diff --git a/definitions/code-vm/models/build.gradle.kts b/definitions/code-vm/models/build.gradle.kts
new file mode 100644
index 000000000..1c4157544
--- /dev/null
+++ b/definitions/code-vm/models/build.gradle.kts
@@ -0,0 +1,82 @@
+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(":definitions:code-vm: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 {
+ namespace = "${Android.codeNamespace}.service.models"
+ compileSdk = Android.compileSdkVersion
+ defaultConfig {
+ minSdk = Android.minSdkVersion
+ targetSdk = Android.targetSdkVersion
+ buildToolsVersion = Android.buildToolsVersion
+ testInstrumentationRunner = Android.testInstrumentationRunner
+ }
+}
+
+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/models/.gitignore b/definitions/code-vm/protos/.gitignore
similarity index 100%
rename from service/models/.gitignore
rename to definitions/code-vm/protos/.gitignore
diff --git a/service/protos/build.gradle.kts b/definitions/code-vm/protos/build.gradle.kts
similarity index 100%
rename from service/protos/build.gradle.kts
rename to definitions/code-vm/protos/build.gradle.kts
diff --git a/definitions/code-vm/protos/src/main/proto/account/v1/code_account_service.proto b/definitions/code-vm/protos/src/main/proto/account/v1/code_account_service.proto
new file mode 100644
index 000000000..d36ed1db8
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/account/v1/code_account_service.proto
@@ -0,0 +1,239 @@
+syntax = "proto3";
+
+package code.account.v1;
+
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/account/v1;account";
+option java_package = "com.codeinc.gen.account.v1";
+option objc_class_prefix = "CPBAccountV1";
+
+import "common/v1/code_model.proto";
+import "transaction/v2/code_transaction_service.proto";
+import "google/protobuf/timestamp.proto";
+
+
+service Account {
+ // IsCodeAccount returns whether an owner account is a Code account. This hints
+ // to the client whether the account can be logged in, used for making payments,
+ // etc.
+ rpc IsCodeAccount(IsCodeAccountRequest) returns (IsCodeAccountResponse);
+
+ // GetTokenAccountInfos returns token account metadata relevant to the Code owner
+ // account.
+ rpc GetTokenAccountInfos(GetTokenAccountInfosRequest) returns (GetTokenAccountInfosResponse);
+
+ // LinkAdditionalAccounts allows a client to declare additional accounts to
+ // be tracked and used within Code. The accounts declared in this RPC are not
+ // managed by Code (ie. not a Timelock account), created externally and cannot
+ // be linked automatically (ie. authority derived off user 12 words).
+ rpc LinkAdditionalAccounts(LinkAdditionalAccountsRequest) returns (LinkAdditionalAccountsResponse);
+}
+
+message IsCodeAccountRequest {
+ // The owner account to check against.
+ common.v1.SolanaAccountId owner = 1;
+
+
+ // The signature is of serialize(IsCodeAccountRequest) without this field set
+ // using the private key of the owner account. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+
+}
+
+message IsCodeAccountResponse {
+ Result result = 1;
+ enum Result {
+ // The account is a Code account.
+ OK = 0;
+ // The account is not a Code account.
+ NOT_FOUND = 1;
+ // The account exists, but at least one timelock account is unlocked.
+ UNLOCKED_TIMELOCK_ACCOUNT = 2;
+ }
+}
+
+message GetTokenAccountInfosRequest {
+ // The owner account, which can also be thought of as a parent account for this
+ // RPC that links to one or more token accounts.
+ common.v1.SolanaAccountId owner = 1;
+
+
+ // The signature is of serialize(GetTokenAccountInfosRequest) without this field set
+ // using the private key of the owner account. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+
+}
+
+message GetTokenAccountInfosResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+
+ map token_account_infos = 2;
+}
+
+message LinkAdditionalAccountsRequest {
+ // The owner account to link to
+ common.v1.SolanaAccountId owner = 1;
+
+
+ // The authority account derived off the user's 12 words, which contains
+ // the USDC ATA (and potentially others in the future) that will be used
+ // in swaps.
+ common.v1.SolanaAccountId swap_authority = 2;
+
+
+ // Signature values for each account provided in this request. Each signature
+ // must be generated without this array set. The expected ordering of signatures:
+ // 1. owner
+ // 2. swap_authority
+ repeated common.v1.Signature signatures = 3 ;
+
+}
+
+message LinkAdditionalAccountsResponse {
+ Result result = 1;
+ enum Result {
+ // Supports idempotency, and will be returned as long as the request exactly
+ // matches a previous execution.
+ OK = 0;
+ // The action has been denied (eg. owner account not phone verified)
+ DENIED = 1;
+ // An account being linked is not valid
+ INVALID_ACCOUNT = 2;
+ }
+}
+
+message TokenAccountInfo {
+ // The token account's address
+ common.v1.SolanaAccountId address = 1;
+
+
+ // The owner of the token account, which can also be thought of as a parent
+ // account that links to one or more token accounts. This is provided when
+ // available.
+ common.v1.SolanaAccountId owner = 2;
+
+ // The token account's authority, which has access to moving funds for the
+ // account. This can be the owner account under certain circumstances (eg.
+ // ATA, primary account). This is provided when available.
+ common.v1.SolanaAccountId authority = 3;
+
+ // The type of token account, which infers its intended use.
+ common.v1.AccountType account_type = 4;
+
+
+ // The account's derivation index for applicable account types. When this field
+ // doesn't apply, a zero value is provided.
+ uint64 index = 5;
+
+ // The source of truth for the balance calculation.
+ BalanceSource balance_source = 6;
+ enum BalanceSource {
+ // The account's balance could not be determined. This may be returned when
+ // the data source is unstable and a reliable balance cannot be determined.
+ BALANCE_SOURCE_UNKNOWN = 0;
+ // The account's balance was fetched directly from a finalized state on the
+ // blockchain.
+ BALANCE_SOURCE_BLOCKCHAIN = 1;
+ // The account's balance was calculated using cached values in Code. Accuracy
+ // is only guaranteed when management_state is LOCKED.
+ BALANCE_SOURCE_CACHE = 2;
+ }
+
+ // The balance in quarks, as observed by Code. This may not reflect the value
+ // on the blockchain and could be non-zero even if the account hasn't been created.
+ // Use balance_source to determine how this value was calculated.
+ uint64 balance = 7;
+
+ // The state of the account as it pertains to Code's ability to manage funds.
+ ManagementState management_state = 8;
+ enum ManagementState {
+ // The state of the account is unknown. This may be returned when the
+ // data source is unstable and a reliable state cannot be determined.
+ MANAGEMENT_STATE_UNKNOWN = 0;
+ // Code does not maintain a management state and won't move funds for this
+ // account.
+ MANAGEMENT_STATE_NONE = 1;
+ // The account is in the process of transitioning to the LOCKED state.
+ MANAGEMENT_STATE_LOCKING = 2;
+ // The account's funds are locked and Code has co-signing authority.
+ MANAGEMENT_STATE_LOCKED = 3;
+ // The account is in the process of transitioning to the UNLOCKED state.
+ MANAGEMENT_STATE_UNLOCKING = 4;
+ // The account's funds are unlocked and Code no longer has co-signing
+ // authority. The account must transition to the LOCKED state to have
+ // management capabilities.
+ MANAGEMENT_STATE_UNLOCKED = 5;
+ // The account is in the process of transitioning to the CLOSED state.
+ MANAGEMENT_STATE_CLOSING = 6;
+ // The account has been closed and doesn't exist on the blockchain.
+ // Subsequently, it also has a zero balance.
+ MANAGEMENT_STATE_CLOSED = 7;
+ }
+
+ // The state of the account on the blockchain.
+ BlockchainState blockchain_state = 9;
+ enum BlockchainState {
+ // The state of the account is unknown. This may be returned when the
+ // data source is unstable and a reliable state cannot be determined.
+ BLOCKCHAIN_STATE_UNKNOWN = 0;
+ // The account does not exist on the blockchain.
+ BLOCKCHAIN_STATE_DOES_NOT_EXIST = 1;
+ // The account is created and exists on the blockchain.
+ BLOCKCHAIN_STATE_EXISTS = 2;
+ }
+
+ // For temporary incoming accounts only. Flag indicates whether client must
+ // actively try rotating it by issuing a ReceivePaymentsPrivately intent. In
+ // general, clients should wait as long as possible until this flag is true
+ // or requiring the funds to send their next payment.
+ bool must_rotate = 10;
+
+ // Whether an account is claimed. This only applies to relevant account types
+ // (eg. REMOTE_SEND_GIFT_CARD).
+ ClaimState claim_state = 11;
+ enum ClaimState {
+ // The account doesn't have a concept of being claimed, or the state
+ // could not be fetched by server.
+ CLAIM_STATE_UNKNOWN = 0;
+ // The account has not yet been claimed.
+ CLAIM_STATE_NOT_CLAIMED = 1;
+ // The account is claimed. Attempting to claim it will fail.
+ CLAIM_STATE_CLAIMED = 2;
+ // The account hasn't been claimed, but is expired. Funds will move
+ // back to the issuer. Attempting to claim it will fail.
+ CLAIM_STATE_EXPIRED = 3;
+ }
+
+ // For account types used as an intermediary for sending money between two
+ // users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange
+ // data used to fund the account. Over time, this value will become stale:
+ // 1. Exchange rates will fluctuate, so the total fiat amount will differ.
+ // 2. External entities can deposit additional funds into the account, so
+ // the balance, in quarks, may be greater than the original quark value.
+ // 3. The balance could have been received, so the total balance can show
+ // as zero.
+ transaction.v2.ExchangeData original_exchange_data = 12;
+
+ // The token account's mint
+ common.v1.SolanaAccountId mint = 13;
+
+ // Reserved for the number of decimals configured for the mint
+ reserved 14;
+
+ // Reserved for a user-friendly display name for the mint
+ reserved 15;
+
+ // The relationship with a third party that this account has established with.
+ // This only applies to relevant account types (eg. RELATIONSHIP).
+ common.v1.Relationship relationship = 16;
+
+ // Time the account was created, if available. For Code accounts, this is
+ // the time of intent submission. Otherwise, for external accounts, it is
+ // the tiem created on the blockchain.
+ google.protobuf.Timestamp created_at = 17;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/badge/v1/code_badge_service.proto b/definitions/code-vm/protos/src/main/proto/badge/v1/code_badge_service.proto
new file mode 100644
index 000000000..f26d20ce4
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/badge/v1/code_badge_service.proto
@@ -0,0 +1,25 @@
+syntax = "proto3";
+package code.badge.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/badge/v1;badge";
+option java_package = "com.codeinc.gen.badge.v1";
+option objc_class_prefix = "CPBBadgeV1";
+import "common/v1/code_model.proto";
+
+service Badge {
+ // ResetBadgeCount resets an owner account's app icon badge count back to zero
+ rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse);
+}
+message ResetBadgeCountRequest {
+ // The owner account to clear badge count
+ common.v1.SolanaAccountId owner = 1;
+ // The signature is of serialize(ResetBadgeCountRequest) without this field set
+ // using the private key of the owner account. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+}
+message ResetBadgeCountResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
diff --git a/definitions/code-vm/protos/src/main/proto/chat/v1/code_chat_service.proto b/definitions/code-vm/protos/src/main/proto/chat/v1/code_chat_service.proto
new file mode 100644
index 000000000..51c4a40bc
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/chat/v1/code_chat_service.proto
@@ -0,0 +1,256 @@
+syntax = "proto3";
+package code.chat.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v1;chat";
+option java_package = "com.codeinc.gen.chat.v1";
+option objc_class_prefix = "CPBChatV1";
+import "common/v1/code_model.proto";
+import "transaction/v2/code_transaction_service.proto";
+import "google/protobuf/timestamp.proto";
+
+// Deprecated: Use the v2 service
+service Chat {
+ // GetChats gets the set of chats for an owner account
+ rpc GetChats(GetChatsRequest) returns (GetChatsResponse);
+ // GetMessages gets the set of messages for a chat
+ rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse);
+ // AdvancePointer advances a pointer in chat history
+ rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse);
+ // SetMuteState configures the mute state of a chat
+ rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse);
+ // SetSubscriptionState configures the susbscription state of a chat
+ rpc SetSubscriptionState(SetSubscriptionStateRequest) returns (SetSubscriptionStateResponse);
+ //
+ // Experimental PoC two-way chat APIs below
+ //
+ rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse);
+ rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
+}
+message GetChatsRequest {
+ common.v1.SolanaAccountId owner = 1;
+ common.v1.Signature signature = 2;
+ uint32 page_size = 3;
+ Cursor cursor = 4;
+ Direction direction = 5;
+ enum Direction {
+ ASC = 0;
+ DESC = 1;
+ }
+}
+message GetChatsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ repeated ChatMetadata chats = 2 ;
+}
+message GetMessagesRequest {
+ ChatId chat_id = 1;
+ common.v1.SolanaAccountId owner = 2;
+ common.v1.Signature signature = 3;
+ uint32 page_size = 4;
+ Cursor cursor = 5;
+ Direction direction = 6;
+ enum Direction {
+ ASC = 0;
+ DESC = 1;
+ }
+}
+message GetMessagesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ repeated ChatMessage messages = 2 ;
+}
+message AdvancePointerRequest {
+ ChatId chat_id = 1;
+ Pointer pointer = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message AdvancePointerResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ CHAT_NOT_FOUND = 1;
+ MESSAGE_NOT_FOUND = 2;
+ }
+}
+message SetMuteStateRequest {
+ ChatId chat_id = 1;
+ bool is_muted = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message SetMuteStateResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ CHAT_NOT_FOUND = 1;
+ CANT_MUTE = 2;
+ }
+}
+message SetSubscriptionStateRequest {
+ ChatId chat_id = 1;
+ bool is_subscribed = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message SetSubscriptionStateResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ CHAT_NOT_FOUND = 1;
+ CANT_UNSUBSCRIBE = 2;
+ }
+}
+message OpenChatEventStream {
+ ChatId chat_id = 1;
+ common.v1.SolanaAccountId owner = 2;
+ common.v1.Signature signature = 3;
+}
+message ChatStreamEvent {
+ repeated ChatMessage messages = 1;
+ repeated Pointer pointers = 2;
+}
+message ChatStreamEventBatch {
+ repeated ChatStreamEvent events = 2 ;
+}
+message StreamChatEventsRequest {
+ oneof type {
+ OpenChatEventStream open_stream = 1;
+ common.v1.ClientPong pong = 2;
+ }
+}
+message StreamChatEventsResponse {
+ oneof type {
+ ChatStreamEventBatch events = 1;
+ common.v1.ServerPing ping = 2;
+ }
+}
+message SendMessageRequest {
+ common.v1.SolanaAccountId owner = 1;
+ common.v1.Signature signature = 2;
+ ChatId chat_id = 3;
+ // todo: What field type should this be? Maybe the chat message itself with fields missing?
+ repeated Content content = 4 ;
+}
+message SendMessageResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ ChatMessage message = 2;
+}
+message ChatId {
+ bytes value = 1 ;
+}
+message ChatMessageId {
+ bytes value = 1 ;
+}
+message ChatMemberId {
+ // todo: Public key for now
+ bytes value = 1 ;
+}
+message Pointer {
+ Kind kind = 1;
+ enum Kind {
+ UNKNOWN = 0;
+ READ = 1;
+ DELIVERED = 2;
+ SENT = 3; // Probably always inferred by OK result in SendMessageResponse
+ }
+ ChatMessageId value = 2;
+ ChatMemberId user = 3;
+}
+message ChatMetadata {
+ ChatId chat_id = 1;
+ // Recommended chat title inferred by the type of chat
+ oneof title {
+ ServerLocalizedContent localized = 2;
+ common.v1.Domain domain = 3;
+ }
+ // Pointer in the chat indicating the most recently read message by the user
+ Pointer read_pointer = 4;
+ // Estimated number of unread messages in this chat
+ uint32 num_unread = 5;
+ // Has the user muted this chat?
+ bool is_muted = 6;
+ // Is the user subscribed to this chat?
+ bool is_subscribed = 7;
+ // Can the user mute this chat?
+ bool can_mute = 8;
+ // Can the user unsubscribe from this chat?
+ bool can_unsubscribe = 9;
+ // Cursor value for this chat for reference in subsequent GetChatsRequest
+ Cursor cursor = 10;
+ // Is this a verified chat?
+ //
+ // Note: It's possible to have two chats with the same title, but with
+ // different verification statuses. They should be treated separately.
+ bool is_verified = 11;
+}
+message ChatMessage {
+ // Unique ID for this message
+ ChatMessageId message_id = 1;
+ // Timestamp this message was generated at
+ google.protobuf.Timestamp ts = 2;
+ // Ordered message content. A message may have more than one piece of content.
+ repeated Content content = 3 ;
+ // Cursor value for this message for reference in subsequent GetMessagesRequest
+ Cursor cursor = 4;
+ ChatMemberId sender = 5;
+}
+message Content {
+ oneof type {
+ ServerLocalizedContent server_localized = 1;
+ ExchangeDataContent exchange_data = 2;
+ NaclBoxEncryptedContent nacl_box = 3;
+ TextContent text = 4;
+ ThankYouContent thank_you = 5;
+ }
+}
+message ServerLocalizedContent {
+ // When server-side localization is in place, clients will always see the
+ // localized text.
+ string key_or_text = 1 ;
+}
+message ExchangeDataContent {
+ Verb verb = 1;
+ enum Verb {
+ UNKNOWN = 0;
+ GAVE = 1;
+ RECEIVED = 2;
+ WITHDREW = 3;
+ DEPOSITED = 4;
+ SENT = 5;
+ RETURNED = 6;
+ SPENT = 7;
+ PAID = 8;
+ PURCHASED = 9;
+ RECEIVED_TIP = 10;
+ SENT_TIP = 11;
+ }
+ oneof exchange_data {
+ transaction.v2.ExchangeData exact = 2;
+ transaction.v2.ExchangeDataWithoutRate partial = 3;
+ }
+}
+message NaclBoxEncryptedContent {
+ common.v1.SolanaAccountId peer_public_key = 1;
+ bytes nonce = 2 ;
+ bytes encrypted_payload = 3 ;
+}
+message TextContent {
+ string text = 1 ;
+}
+message ThankYouContent {
+ // todo: May need additional metdata (who is being thanked, which tip, etc)
+}
+// Opaque cursor used across paged APIs. Underlying bytes may change as paging
+// strategies evolve.
+message Cursor {
+ bytes value = 1 ;
+}
diff --git a/service/protos/src/main/proto/chat/v2/chat_service.proto b/definitions/code-vm/protos/src/main/proto/chat/v2/chat_service.proto
similarity index 88%
rename from service/protos/src/main/proto/chat/v2/chat_service.proto
rename to definitions/code-vm/protos/src/main/proto/chat/v2/chat_service.proto
index 47003e0d1..bce403d8e 100644
--- a/service/protos/src/main/proto/chat/v2/chat_service.proto
+++ b/definitions/code-vm/protos/src/main/proto/chat/v2/chat_service.proto
@@ -3,8 +3,8 @@ package code.chat.v2;
option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v2;chat";
option java_package = "com.codeinc.gen.chat.v2";
option objc_class_prefix = "CPBChatV2";
-import "common/v1/model.proto";
-import "transaction/v2/transaction_service.proto";
+import "common/v1/code_model.proto";
+import "transaction/v2/code_transaction_service.proto";
import "google/protobuf/timestamp.proto";
service Chat {
@@ -83,7 +83,7 @@ message GetChatsResponse {
repeated ChatMetadata chats = 2 ;
}
message GetMessagesRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2;
common.v1.SolanaAccountId owner = 3;
common.v1.Signature signature = 4;
@@ -106,7 +106,7 @@ message GetMessagesResponse {
repeated ChatMessage messages = 2 ;
}
message OpenChatEventStream {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2 ;
common.v1.SolanaAccountId owner = 3;
common.v1.Signature signature = 4;
@@ -144,54 +144,32 @@ message StreamChatEventsResponse {
message StartChatRequest {
common.v1.SolanaAccountId owner = 1;
common.v1.Signature signature = 2;
- ChatMemberIdentity self = 3;
oneof parameters {
- StartTwoWayChatParameters two_way_chat = 4;
+ StartTipChatParameters tip_chat = 3;
// GroupChatParameters group_chat = 4;
}
}
-// StartTwoWayChatParameters contains the parameters required to start
-// or recover a two way chat between the caller and the specified 'other_user'.
-//
-// The 'other_user' is currently the 'tip_address', normally retrieved from
-// user.Identity.GetTwitterUser(username).
-message StartTwoWayChatParameters {
- // The account id of the user the caller wishes to chat with.
- //
- // This will be the `tip` (or equivalent) address.
- common.v1.SolanaAccountId other_user = 1;
- // The intent_id of the payment that initiated the chat/friendship.
- //
- // This field is optional. It is used as an optimization when the server has not
- // yet observed the establishment of a friendship. In this case, the server will
- // use the provided intent_id to verify the friendship.
- //
- // This is most likely to occur when initiating a chat with a user for the first
- // time.
- common.v1.IntentId intent_id = 2;
- // The identity of the other user.
- //
- // Note: This can/should be removed with proper intent plumbing.
- ChatMemberIdentity identity = 3;
+// Starts a two-way chat between a tipper and tippee. Chat members are
+// inferred from the 12 word public keys involved in the intent. Only
+// the tippee can start the chat, and the tipper is anonymous if this
+// is the first between the involved Code users.
+message StartTipChatParameters {
+ // The tip's intent ID, which can be extracted from the reference in
+ // an ExchangeDataContent message content where the verb is RECEIVED_TIP.
+ common.v1.IntentId intent_id = 1;
}
message StartChatResponse {
Result result = 1;
enum Result {
- OK = 0;
- // DENIED indicates the caller is not allowed to start/join the chat.
- DENIED = 1;
- // INVALID_PRAMETER indicates one of the parameters is invalid.
+ OK = 0;
+ DENIED = 1;
INVALID_PARAMETER = 2;
- // PENDING indicates that the payment (for chat) intent is pending confirmation
- // before the service will permit the creation of the chat. This can happen in
- // cases where the block chain is particularly slow (beyond our RPC timeouts).
- PENDING = 3;
}
- // The chat to use if the RPC was successful.
+ // The chat to use if the RPC was successful
ChatMetadata chat = 2;
}
message SendMessageRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2 ;
// Allowed content types that can be sent by client:
// - TextContent
@@ -214,7 +192,7 @@ message SendMessageResponse {
ChatMessage message = 2;
}
message AdvancePointerRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
Pointer pointer = 2;
common.v1.SolanaAccountId owner = 3;
common.v1.Signature signature = 4;
@@ -230,7 +208,7 @@ message AdvancePointerResponse {
}
}
message RevealIdentityRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2;
ChatMemberIdentity identity = 3;
common.v1.SolanaAccountId owner = 4;
@@ -248,7 +226,7 @@ message RevealIdentityResponse {
ChatMessage message = 2;
}
message SetMuteStateRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2 ;
bool is_muted = 3;
common.v1.SolanaAccountId owner = 4;
@@ -264,7 +242,7 @@ message SetMuteStateResponse {
}
}
message SetSubscriptionStateRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2 ;
bool is_subscribed = 3;
common.v1.SolanaAccountId owner = 4;
@@ -280,7 +258,7 @@ message SetSubscriptionStateResponse {
}
}
message NotifyIsTypingRequest {
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
ChatMemberId member_id = 2 ;
bool is_typing = 3;
common.v1.SolanaAccountId owner = 4;
@@ -294,6 +272,11 @@ message NotifyIsTypingResponse {
CHAT_NOT_FOUND = 2;
}
}
+message ChatId {
+ // Sufficient space is left for a consistent hash value, though other types
+ // of values may be used.
+ bytes value = 1 ;
+}
message ChatMessageId {
// Guaranteed to be a time-based UUID. This should be used to construct a
// consistently ordered message history based on time using a simple byte
@@ -325,7 +308,7 @@ enum PointerType {
// todo: Support is_verified in a clean way
message ChatMetadata {
// Globally unique ID for this chat
- common.v1.ChatId chat_id = 1;
+ ChatId chat_id = 1;
// The type of chat
ChatType type = 2 ;
// The chat title, which will be localized by server when applicable
diff --git a/definitions/code-vm/protos/src/main/proto/common/v1/code_model.proto b/definitions/code-vm/protos/src/main/proto/common/v1/code_model.proto
new file mode 100644
index 000000000..6d681311e
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/common/v1/code_model.proto
@@ -0,0 +1,134 @@
+syntax = "proto3";
+package code.common.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/common/v1;common";
+option java_package = "com.codeinc.gen.common.v1";
+option objc_class_prefix = "CPBCommonV1";
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+
+// AccountType associates a type to an account, which infers how an account is used
+// within the Code ecosystem.
+enum AccountType {
+ UNKNOWN = 0;
+ PRIMARY = 1;
+ TEMPORARY_INCOMING = 2;
+ TEMPORARY_OUTGOING = 3;
+ BUCKET_1_KIN = 4;
+ BUCKET_10_KIN = 5;
+ BUCKET_100_KIN = 6;
+ BUCKET_1_000_KIN = 7;
+ BUCKET_10_000_KIN = 8;
+ BUCKET_100_000_KIN = 9;
+ BUCKET_1_000_000_KIN = 10;
+ LEGACY_PRIMARY_2022 = 11;
+ REMOTE_SEND_GIFT_CARD = 12;
+ RELATIONSHIP = 13;
+ SWAP = 14;
+}
+// SolanaAccountId is a raw binary Ed25519 public key for a Solana account
+message SolanaAccountId {
+ bytes value = 1 ;
+}
+// InstructionAccount is an account public key used within the context of
+// an instruction.
+message InstructionAccount {
+ SolanaAccountId account = 1;
+ bool is_signer = 2;
+ bool is_writable = 3;
+}
+// Transaction is a raw binary Solana transaction
+message Transaction {
+ // Maximum size taken from: https://github.com/solana-labs/solana/blob/39b3ac6a8d29e14faa1de73d8b46d390ad41797b/sdk/src/packet.rs#L9-L13
+ bytes value = 1 ;
+}
+// Blockhash is a raw binary Solana blockchash
+message Blockhash {
+ bytes value = 1 ;
+}
+// Signature is a raw binary Ed25519 signature
+message Signature {
+ bytes value = 1 ;
+}
+// IntentId is a client-side generated ID that maps to an intent to perform actions
+// on the blockchain fulfilled by the Code sequencer.
+message IntentId {
+ bytes value = 1 ;
+}
+// UserId is a globally unique identifier for a user within Code
+//
+// Note: Users outside Code are modelled as relationship accounts
+message UserId {
+ bytes value = 1 ;
+}
+// DataContainerId is a globally unique identifier for a container where a user
+// can store a copy of their data
+message DataContainerId {
+ bytes value = 1 ;
+}
+// DeviceToken is an opaque token used to verify whether a device real
+message DeviceToken {
+ string value = 1 ;
+}
+// AppInstallId is a unque ID tied to a client app installation. It does not
+// identify a device. Value should remain private and not be shared across
+// installs.
+message AppInstallId {
+ string value = 1 ;
+}
+// PhoneNumber is an E.164 phone number
+message PhoneNumber {
+ // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164
+ string value = 1;
+}
+// Domain is a hostname
+message Domain {
+ string value = 1 ;
+}
+// Relationship is a set of identifiers that a user can establish a relationship
+// with.
+message Relationship {
+ oneof type {
+ common.v1.Domain domain = 1;
+ }
+}
+// Hash is a raw binary 32 byte hash value
+message Hash {
+ bytes value = 1 ;
+}
+// Locale is a user locale consisting of a combination of language, script and region
+message Locale {
+ string value = 1;
+}
+// UUID is a 16 byte UUID value
+message UUID {
+ bytes value = 1 ;
+}
+// Request is a generic wrapper for gRPC requests
+message Request {
+ string version = 1;
+ string service = 2;
+ string method = 3;
+ bytes body = 4;
+}
+// Response is a generic wrapper for gRPC responses
+message Response {
+ Result result = 1;
+ bytes body = 2;
+ string message = 3;
+ enum Result {
+ OK = 0;
+ ERROR = 1;
+ }
+}
+message ServerPing {
+ // Timestamp the ping was sent on the stream, for client to get a sense
+ // of potential network latency
+ google.protobuf.Timestamp timestamp = 1;
+ // The delay server will apply before sending the next ping
+ google.protobuf.Duration ping_delay = 2;
+}
+message ClientPong {
+ // Timestamp the Pong was sent on the stream, for server to get a sense
+ // of potential network latency
+ google.protobuf.Timestamp timestamp = 1;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto b/definitions/code-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto
new file mode 100644
index 000000000..c28545e59
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto
@@ -0,0 +1,106 @@
+syntax = "proto3";
+package code.contact.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/contact/v1;contact";
+option java_package = "com.codeinc.gen.contact.v1";
+option objc_class_prefix = "CPBContactV1";
+import "common/v1/code_model.proto";
+
+service ContactList {
+ // AddContacts adds a batch of contacts to a user's contact list
+ rpc AddContacts(AddContactsRequest) returns (AddContactsResponse);
+ // RemoveContacts removes a batch of contacts from a user's contact list
+ rpc RemoveContacts(RemoveContactsRequest) returns (RemoveContactsResponse);
+ // GetContacts gets a subset of contacts from a user's contact list
+ rpc GetContacts(GetContactsRequest) returns (GetContactsResponse);
+}
+message AddContactsRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(AddContactsRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The data container for the copy of the contact list being added to.
+ common.v1.DataContainerId container_id = 3;
+ // The set of contacts to add to the contact list
+ repeated common.v1.PhoneNumber contacts = 4 ;
+
+}
+message AddContactsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ // The contacts' current status keyed by phone number. This is an optimization
+ // so that clients can populate initial state without needing an extra network
+ // call.
+ map contact_status = 2;
+}
+message RemoveContactsRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(RemoveContactsRequest) without this field
+ // set using the private key of owner_account_id. This provides an
+ // authentication mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The data container for the copy of the contact list being removed from.
+ common.v1.DataContainerId container_id = 3;
+ // The set of contacts to remove from the contact list
+ repeated common.v1.PhoneNumber contacts = 4 ;
+}
+message RemoveContactsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
+message GetContactsRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(GetContactsRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The data container for the copy of the contact list being fetched.
+ common.v1.DataContainerId container_id = 3;
+ // The page token, which is retreived from a previous response, to get the next
+ // set of contacts. The first page is returned when not set.
+ PageToken page_token = 4;
+ // Filter out contacts that have an association with Code. This includes users
+ // that have both been invited and registered with the app.
+ bool include_only_in_app_contacts = 5;
+}
+message GetContactsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ // A page of contacts
+ repeated Contact contacts = 2;
+ // The page token to include in a subsequent request to get the next set of
+ // contacts. This will not be set for the last response in the list of
+ // pages.
+ PageToken next_page_token = 3;
+}
+message Contact {
+ // The contact's phone number
+ common.v1.PhoneNumber phone_number = 1;
+ // The contact's current status
+ ContactStatus status = 2;
+}
+message ContactStatus {
+ // Flag to indicate whether a user has registered with Code and used the app
+ // at least once.
+ bool is_registered = 1;
+ // Flag to indicate whether a user has been invited to Code.
+ //
+ // todo: This field will be deprecated after the invite phase is complete.
+ bool is_invited = 2;
+ // Flag to indicate whether a user's invitation to Code has been revoked.
+ //
+ // todo: This field will be deprecated after the invite phase is complete.
+ bool is_invite_revoked = 3;
+}
+message PageToken {
+ bytes value = 1 ;
+}
diff --git a/service/protos/src/main/proto/currency/v1/currency_service.proto b/definitions/code-vm/protos/src/main/proto/currency/v1/code_currency_service.proto
similarity index 100%
rename from service/protos/src/main/proto/currency/v1/currency_service.proto
rename to definitions/code-vm/protos/src/main/proto/currency/v1/code_currency_service.proto
diff --git a/definitions/code-vm/protos/src/main/proto/device/v1/code_device_service.proto b/definitions/code-vm/protos/src/main/proto/device/v1/code_device_service.proto
new file mode 100644
index 000000000..37e2daf74
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/device/v1/code_device_service.proto
@@ -0,0 +1,49 @@
+syntax = "proto3";
+package code.device.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/device/v1;device";
+option java_package = "com.codeinc.gen.device.v1";
+option objc_class_prefix = "CPBDevicetV1";
+import "common/v1/code_model.proto";
+
+service Device {
+ // RegisterLoggedInAccounts registers a set of owner accounts logged for
+ // an app install. Currently, a single login is enforced per app install.
+ // After using GetLoggedInAccounts to detect stale logins, clients can use
+ // this RPC to update the set of accounts with valid login sessions.
+ rpc RegisterLoggedInAccounts(RegisterLoggedInAccountsRequest) returns (RegisterLoggedInAccountsResponse);
+ // GetLoggedInAccounts gets the set of logged in accounts for an app install.
+ // Clients can use this RPC to detect stale logins for boot out of the app.
+ rpc GetLoggedInAccounts(GetLoggedInAccountsRequest) returns (GetLoggedInAccountsResponse);
+}
+message RegisterLoggedInAccountsRequest {
+ common.v1.AppInstallId app_install = 1;
+ // The set of owners logged into the app install. Setting an empty value
+ // indicates there are no logged in users. We allow for more than one owner
+ // in the spec with a repeated field to be flexible in the future.
+ repeated common.v1.SolanaAccountId owners = 2 ;
+ // Signature values must appear in the exact order their respecitive signing
+ // owner account appears in the owners field. All signatures should be generated
+ // without any other signature values set.
+ repeated common.v1.Signature signatures = 3 ;
+}
+message RegisterLoggedInAccountsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ INVALID_OWNER = 1;
+ }
+ // Set of invalid owner accounts detected in the request. An owner account
+ // can be invalid for several reasons: not phone verified, timelock account
+ // unlocked, etc. Value is set when result is INVALID_OWNER.
+ repeated common.v1.SolanaAccountId invalid_owners = 2 ;
+}
+message GetLoggedInAccountsRequest {
+ common.v1.AppInstallId app_install = 1;
+}
+message GetLoggedInAccountsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ repeated common.v1.SolanaAccountId owners = 2 ;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/invite/v2/code_invite_service.proto b/definitions/code-vm/protos/src/main/proto/invite/v2/code_invite_service.proto
new file mode 100644
index 000000000..19936594d
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/invite/v2/code_invite_service.proto
@@ -0,0 +1,91 @@
+syntax = "proto3";
+package code.invite.v2;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/invite/v2;invite";
+option java_package = "com.codeinc.gen.invite.v2";
+option objc_class_prefix = "CPBInviteV2";
+import "common/v1/code_model.proto";
+
+service Invite {
+ // GetInviteCount gets the number of invites that a user can send out.
+ rpc GetInviteCount(GetInviteCountRequest) returns (GetInviteCountResponse);
+ // InvitePhoneNumber invites someone to join via their phone number. A phone number
+ // can only be invited once by a unique user or invite code. This is to avoid having
+ // a phone number consuming more than one invite count globally.
+ rpc InvitePhoneNumber(InvitePhoneNumberRequest) returns (InvitePhoneNumberResponse);
+ // GetInvitationStatus gets a phone number's invitation status.
+ rpc GetInvitationStatus(GetInvitationStatusRequest) returns (GetInvitationStatusResponse);
+}
+message GetInviteCountRequest {
+ // The user to query for their invite count
+ common.v1.UserId user_id = 1;
+}
+message GetInviteCountResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ // The number of invites the user is allowed to issue.
+ uint32 invite_count = 2;
+}
+message InvitePhoneNumberRequest {
+ // The source for the invite. One of these values must be present
+ oneof source {
+ common.v1.UserId user = 1;
+ InviteCode invite_code = 3;
+ }
+ // The phone number receiving the invite.
+ common.v1.PhoneNumber receiver = 2;
+}
+message InvitePhoneNumberResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The source exceeded their invite count and is restricted from issuing
+ // further invites.
+ INVITE_COUNT_EXCEEDED = 1;
+ // The receiver phone number has already been invited. Regardless of who
+ // invited it, the source's invite count is not decremented when this is
+ // returned.
+ ALREADY_INVITED = 2;
+ // The source user has not been invited.
+ USER_NOT_INVITED = 3;
+ // The receiver phone number failed validation.
+ INVALID_RECEIVER_PHONE_NUMBER = 4;
+ // The invite code doesn't exist.
+ INVITE_CODE_NOT_FOUND = 5;
+ // The invite code has been revoked.
+ INVITE_CODE_REVOKED = 6;
+ // The invite code has expired.
+ INVITE_CODE_EXPIRED = 7;
+ }
+}
+message GetInvitationStatusRequest {
+ // The user being queried for their invitation status.
+ common.v1.UserId user_id = 1;
+}
+message GetInvitationStatusResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ // The user's invitation status
+ InvitationStatus status = 2;
+}
+message InviteCode {
+ // Regex for invite codes
+ string value = 1 ;
+}
+message PageToken {
+ bytes value = 1 ;
+}
+enum InvitationStatus {
+ // The phone number has never been invited.
+ NOT_INVITED = 0;
+ // The phone number has been invited at least once.
+ INVITED = 1;
+ // The phone number has been invited and used the app at least once via a
+ // phone verified account creation or login.
+ REGISTERED = 2;
+ // The phone number was invited, but revoked at a later time.
+ REVOKED = 3;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto b/definitions/code-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto
new file mode 100644
index 000000000..e75005d68
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto
@@ -0,0 +1,330 @@
+syntax = "proto3";
+package code.messaging.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1;messaging";
+option java_package = "com.codeinc.gen.messaging.v1";
+option objc_class_prefix = "CPBMessagingV1";
+import "common/v1/code_model.proto";
+import "transaction/v2/code_transaction_service.proto";
+
+import "google/protobuf/timestamp.proto";
+service Messaging {
+ // OpenMessageStream opens a stream of messages. Messages are routed using the
+ // public key of a rendezvous keypair derived by both the sender and the
+ // recipient of the messages. The sender may be a client or server.
+ //
+ // Messages are expected to be acked once they have been processed by the client.
+ // Ack'd messages will no longer be delivered on future OpenMessageStream calls,
+ // and are eligible for deletion from the service. Clients should, however, handle
+ // duplicate delivery of messages.
+ //
+ // For grabbing a bill, the expected flow is as follows:
+ // 1. The payment sender creates a cash scan code
+ // 2. The payment sender calls OpenMessageStream on the rendezvous public key, which is
+ // derived by using sha256(scan payload) as the keypair seed.
+ // 3. The payment recipient scans the code and uses SendMessage to send their account ID
+ // back to the sender via the rendezvous public key.
+ // 4. The payment sender receives the message, submits the intent, and closes the stream.
+ //
+ // For receiving a bill of requested value, the expected flow is as follows:
+ // 1. The payment recipient uses SendMessage to send their account ID and payment amount to
+ // the sender via the rendezvous public key, which is derived by using sha256(scan payload)
+ // as the keypair seed.
+ // 2. The payment recipient calls OpenMessageStream on the rendezvous public key to listen
+ // for status messages generated by client/server. It must ignore the original message it sent
+ // as part of step 1.
+ // 3. The payment recipient creates a payment request scan code
+ // 4. The payment sender calls PollMessages on the rendezvous public key. This is ok because
+ // we know the message exists per step 1, and doesn't actually incur a long poll. This is a
+ // required hack because we don't have the infrastructure in place to allow multiple listens
+ // on the same stream, and the recipient needs real-time status updates.
+ // 5. The payment sender receives the message (any status messages are ignored), and submits the
+ // intent.
+ // 6. The payment recipient observes status message (eg. IntentSubmitted, ClientRejectedPayment,
+ // WebhookCalled) for payment state.
+ // 7. The payment recipient closes the stream once the payment hits a terminal state, or times out.
+ //
+ // For logging in, the expected flow is as follows:
+ // 1. The third party uses SendMessage to send their login challenge to the user via the rendezvous
+ // public key, which is derived by using sha256(scan payload) as the keypair seed.
+ // 2. The third party calls OpenMessageStream on the rendezvous public key to listen for status
+ // messages generated by server. It must ignore the original message it sent as part of step 1.
+ // 3. The third party creates a login scan code
+ // 4. The user logging in calls PollMessages on the rendezvous public key. This is ok because
+ // we know the message exists per step 1, and doesn't actually incur a long poll. This is a
+ // required hack because we don't have the infrastructure in place to allow multiple listens
+ // on the same stream, and the recipient needs real-time status updates.
+ // 5. The user logging in receives the message (any status messages are ignored), verifies it,
+ // then submits a login attempt.
+ // 6. The third party observes status message (eg. IntentSubmitted, ClientRejectedLogin,
+ // WebhookCalled) for login state.
+ // 7. The third party closes the stream once the login hits a terminal state, or times out.
+ rpc OpenMessageStream(OpenMessageStreamRequest) returns (stream OpenMessageStreamResponse);
+ // OpenMessageStreamWithKeepAlive is like OpenMessageStream, but enables a ping/pong
+ // keepalive to determine the health of the stream at both the client and server.
+ //
+ // The keepalive protocol is as follows:
+ // 1. Client initiates a stream by sending an OpenMessageStreamRequest.
+ // 2. Upon stream initialization, server begins the keepalive protocol.
+ // 3. Server sends a ping to the client.
+ // 4. Client responds with a pong as fast as possible, making note of
+ // the delay for when to expect the next ping.
+ // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated
+ // or is deemed to be unhealthy.
+ //
+ // Client notes:
+ // * Client should be careful to process messages async, so any responses to pings are
+ // not delayed.
+ // * Clients should implement a reasonable backoff strategy upon continued timeout failures.
+ // * Clients that abuse pong messages may have their streams terminated by server.
+ //
+ // At any point in the stream, server will respond with messages in real time as
+ // they are observed. Messages sent over the stream should not affect the ping/pong
+ // protocol timings. Individual protocols for payment flows remain the same, and are
+ // documented in OpenMessageStream.
+ //
+ // Note: This API will enforce OpenMessageStreamRequest.signature is set as part of migration
+ // to this newer protocol
+ rpc OpenMessageStreamWithKeepAlive(stream OpenMessageStreamWithKeepAliveRequest) returns (stream OpenMessageStreamWithKeepAliveResponse);
+ // PollMessages is like OpenMessageStream, but uses a polling flow for receiving
+ // messages. Updates are not real-time and depedent on the polling interval.
+ // This RPC supports all message types.
+ //
+ // This is a temporary RPC until OpenMessageStream can be built out generically on
+ // both client and server, while supporting things like multiple listeners.
+ rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse);
+ // AckMessages acks one or more messages that have been successfully delivered to
+ // the client.
+ rpc AckMessages(AckMessagesRequest) returns (AckMesssagesResponse);
+ // SendMessage sends a message.
+ rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
+}
+message OpenMessageStreamRequest {
+ RendezvousKey rendezvous_key = 1;
+ // The signature is of serialize(OpenMessageStreamRequest) using rendezvous_key.
+ //
+ // todo: Make required once clients migrate
+ common.v1.Signature signature = 2;
+}
+message OpenMessageStreamResponse {
+ repeated Message messages = 1 ;
+}
+message OpenMessageStreamWithKeepAliveRequest {
+ oneof request_or_pong {
+ OpenMessageStreamRequest request = 1;
+ common.v1.ClientPong pong = 2;
+ }
+}
+message OpenMessageStreamWithKeepAliveResponse {
+ oneof response_or_ping {
+ OpenMessageStreamResponse response = 1;
+ common.v1.ServerPing ping = 2;
+ }
+}
+message PollMessagesRequest {
+ RendezvousKey rendezvous_key = 1;
+ // The signature is of serialize(PollMessagesRequest) using rendezvous_key.
+ common.v1.Signature signature = 2;
+}
+message PollMessagesResponse {
+ repeated Message messages = 1 ;
+}
+message AckMessagesRequest {
+ RendezvousKey rendezvous_key = 1;
+ repeated MessageId message_ids = 2 ;
+}
+message AckMesssagesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
+message SendMessageRequest {
+ // The message to send. Types of messages clients can send are restricted.
+ Message message = 1;
+ // The rendezvous key that the message should be routed to.
+ RendezvousKey rendezvous_key = 2;
+ // The signature is of serialize(Message) using the PrivateKey of the keypair.
+ common.v1.Signature signature = 3;
+}
+message SendMessageResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NO_ACTIVE_STREAM = 1;
+ }
+ // Set if result == OK.
+ MessageId message_id = 2;
+}
+// RendezvousKey is a unique key pair, typically derived from a scan code payload,
+// which is used to establish a secure communication channel anonymously to coordinate
+// a flow using messages.
+message RendezvousKey {
+ bytes value = 1 ;
+}
+// MessageId identifies a message. It is only guaranteed to be unique when
+// paired with a destination (i.e. the rendezvous public key).
+message MessageId {
+ bytes value = 1 ;
+}
+// Request that a pulled out bill be sent to the requested address.
+//
+// This message type is only initiated by clients.
+message RequestToGrabBill {
+ // Requestor is the Kin token account on Solana to which a payment should be sent.
+ common.v1.SolanaAccountId requestor_account = 1;
+}
+// Request that a bill of a requested value is created and sent to the requested
+// address.
+//
+// This message type is only initiated by clients.
+message RequestToReceiveBill {
+ // Requestor is the Kin token account on Solana to which a payment should be sent.
+ common.v1.SolanaAccountId requestor_account = 1;
+ // The exchange data for the requested bill value.
+ oneof exchange_data {
+ // An exact amount of Kin. Payment is guaranteed to transfer the specified
+ // quarks in the requested currency and exchange rate.
+ //
+ // Only supports Kin. Use exchange_data.partial for fiat amounts.
+ transaction.v2.ExchangeData exact = 2;
+ // Fiat amount request. The amount of Kin is determined at time of payment
+ // with a recent exchange rate provided by the paying client and validatd
+ // by server.
+ //
+ // Only supports fiat amounts. Use exchange_data.exact for Kin.
+ transaction.v2.ExchangeDataWithoutRate partial = 3;
+ }
+ //
+ // Optional fields below to identify a domain requesting to receive a bill.
+ // Verification of the domain is optional. When verified, clients can establish
+ // relationships and third parties will by able to identify users with that
+ // account after payment is made.
+ //
+ // Note on field requirements:
+ // - Verified: All of domain, verifier, signature and rendezvous_key are required
+ // - Unverified: Only domain is requried
+ //
+ // The third-party's domain name, which is its primary identifier. Server
+ // guarantees to perform domain verification against the verifier account.
+ common.v1.Domain domain = 4;
+ // Owner account owned by the third party used in domain verification.
+ common.v1.SolanaAccountId verifier = 5;
+ // Signature of this message using the verifier private key, which in addition
+ // to domain verification, authenticates the third party.
+ common.v1.Signature signature = 6;
+ // Rendezvous key to avoid replay attacks
+ RendezvousKey rendezvous_key = 7;
+ // Additional fee payments splitting the requested amount. This is in addition
+ // to the hard-coded Code $0.01 USD fee.
+ repeated transaction.v2.AdditionalFeePayment additional_fees = 8;
+}
+// A status update on a stream to indicate a scan code was scanned. This can appear
+// multiple times for the same stream.
+//
+// This message type is only initiated by client
+message CodeScanned {
+ // Timestamp the client scanned the code
+ google.protobuf.Timestamp timestamp = 1;
+}
+// Payment is rejected by the client
+//
+// This message type is only initiated by clients
+message ClientRejectedPayment {
+ common.v1.IntentId intent_id = 1;
+}
+// Intent was submitted via SubmitIntent
+//
+// This message type is only initiated by server
+message IntentSubmitted {
+ common.v1.IntentId intent_id = 1;
+ // Metadata is available for intents where it can be safely propagated publicly.
+ // Anything else requires an additional authenticated RPC call (eg. login).
+ transaction.v2.Metadata metadata = 2;
+}
+// Webhook was successfully called
+//
+// This message type is only initiated by server
+message WebhookCalled {
+ // Estimated time webhook was received
+ google.protobuf.Timestamp timestamp = 1;
+}
+// Request that an account logs in
+//
+// This message type is only initiated by third-parties through the SDK.
+message RequestToLogin {
+ // The third-party's domain name, which is its primary identifier. Server
+ // guarantees to perform domain verification against the verifier account.
+ //
+ // Clients should expect subdomains for future feature compatiblity, but must
+ // use the ASCII base domain in the RELATIONSHIP account derivation strategy.
+ common.v1.Domain domain = 1;
+ // Deprecated nonce value, which is replaced by the rendezvous_key field which
+ // is effectively derived off a random nonce.
+ reserved 2;
+ // Reserved for a timestamp field, which may be used in the future.
+ reserved 3;
+ // Owner account owned by the third party used in domain verification.
+ common.v1.SolanaAccountId verifier = 4;
+ // Signature of this message using the verifier private key, which in addition
+ // to domain verification, authenticates the third party.
+ common.v1.Signature signature = 5;
+ // Rendezvous key to avoid replay attacks
+ RendezvousKey rendezvous_key = 6;
+}
+// Login is rejected by the client
+//
+// This message type is only initiated by user clients
+message ClientRejectedLogin {
+ // Timestamp the login was rejected
+ google.protobuf.Timestamp timestamp = 4;
+}
+// Client has received an aidrop from server
+//
+// This message type is only initiated by server.
+message AirdropReceived {
+ // The type of airdrop received
+ transaction.v2.AirdropType airdrop_type = 1;
+ // Exchange data relating to the amount of Kin and fiat value of the airdrop
+ transaction.v2.ExchangeData exchange_data = 2;
+ // Time the airdrop was received
+ google.protobuf.Timestamp timestamp = 3;
+}
+message Message {
+ // MessageId is the Id of the message. This ID is generated by the
+ // server, and will _always_ be set when receiving a message.
+ //
+ // Server generates the message to:
+ // 1. Reserve the ability for any future ID changes
+ // 2. Prevent clients attempting to collide message IDs.
+ MessageId id = 1;
+ // The signature sent from SendMessageRequest, which will be injected by server.
+ // This enables clients to ensure no MITM attacks were performed to hijack contents
+ // of the typed message. This is only applicable for messages not generated by server.
+ common.v1.Signature send_message_request_signature = 3;
+ // Next field number is 13
+ oneof kind {
+ //
+ // Section: Cash
+ //
+ RequestToGrabBill request_to_grab_bill = 2;
+ //
+ // Section: Payment Requests
+ //
+ RequestToReceiveBill request_to_receive_bill = 5;
+ CodeScanned code_scanned = 6;
+ ClientRejectedPayment client_rejected_payment = 7;
+ IntentSubmitted intent_submitted = 8;
+ WebhookCalled webhook_called = 9;
+ //
+ // Section: Login
+ //
+ RequestToLogin request_to_login = 10;
+ ClientRejectedLogin client_rejected_login = 12;
+ //
+ // Section: Airdrops
+ //
+ AirdropReceived airdrop_received = 4;
+ }
+ // Reserved for deprecated LoginAttempt field
+ reserved 11;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto b/definitions/code-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto
new file mode 100644
index 000000000..68a38719e
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto
@@ -0,0 +1,107 @@
+syntax = "proto3";
+package code.micropayment.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1;micropayment";
+option java_package = "com.codeinc.gen.micropayment.v1";
+option objc_class_prefix = "APBMicroPaymentV1";
+import "common/v1/code_model.proto";
+
+// todo: Migrate this to a generic "request" service
+service MicroPayment {
+ // GetStatus gets basic request status
+ rpc GetStatus(GetStatusRequest) returns (GetStatusResponse);
+ // RegisterWebhook registers a webhook for a request
+ //
+ // todo: Once Kik codes can encode the entire payment request details, we can
+ // remove the messaging service component and have a Create RPC that
+ // reserves the intent ID with payment details, plus registers the webhook
+ // at the same time. Until that's possible, we're stuck with two RPC calls.
+ rpc RegisterWebhook(RegisterWebhookRequest) returns (RegisterWebhookResponse);
+ // Codify adds a trial micro paywall to any URL
+ rpc Codify(CodifyRequest) returns (CodifyResponse);
+ // GetPathMetadata gets codified website metadata for a given path
+ //
+ // Important Note: This RPC's current implementation is insecure and
+ // it's sole design is to enable PoC and trials.
+ rpc GetPathMetadata(GetPathMetadataRequest) returns (GetPathMetadataResponse);
+}
+message GetStatusRequest {
+ common.v1.IntentId intent_id = 1;
+}
+message GetStatusResponse {
+ // Does the payment request exist?
+ bool exists = 1;
+ // Has the user scanned the code at least once?
+ bool code_scanned = 2;
+ // Has the user sumbmitted a payment?
+ bool intent_submitted = 3;
+}
+message RegisterWebhookRequest {
+ common.v1.IntentId intent_id = 1;
+ string url = 2 ;
+}
+message RegisterWebhookResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // A webhook has already been registered
+ ALREADY_REGISTERED = 1;
+ // A request does not exist for the provided intent ID
+ REQUEST_NOT_FOUND = 2;
+ // A user has already submitted a payment
+ INTENT_EXISTS = 3;
+ // The webhook URL is invalid
+ INVALID_URL = 4;
+ }
+}
+message CodifyRequest {
+ // The URL to Codify
+ string url = 1 ;
+ // ISO 4217 alpha-3 currency code the payment should be made in
+ string currency = 2;
+ // The amount that should be paid in the native currency
+ double native_amount = 3;
+ // The verified owner account public key
+ common.v1.SolanaAccountId owner_account = 4;
+ // The primary account public key where payment will be sent
+ common.v1.SolanaAccountId primary_account = 5;
+;
+ // The signature is of serialize(CodifyRequest) without this field set using the
+ // private key of the owner account. This provides an authentication mechanism
+ // to the RPC and can be used to validate payment details.
+ common.v1.Signature signature = 6;
+}
+message CodifyResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The URL to Codify is invalid
+ INVALID_URL = 1;
+ // The primary account is invalid
+ INVALID_ACCOUNT = 2;
+ // The currency isn't supported for micro payments
+ UNSUPPORTED_CURRENCY = 3;
+ // The payment amount exceeds the minimum/maximum allowed amount
+ NATIVE_AMOUNT_EXCEEDS_LIMIT = 4;
+ }
+ // The URL to view the content with a Code micro paywall
+ string codified_url = 2;
+}
+message GetPathMetadataRequest {
+ string path = 1;
+}
+message GetPathMetadataResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ // The account where the payment should be sent to
+ common.v1.SolanaAccountId destination = 2;
+
+ // ISO 4217 alpha-3 currency code the payment should be made in
+ string currency = 3;
+ // The amount that should be paid in the native currency
+ double native_amount = 4;
+ // The URL to redirect upon successful payment
+ string redirct_url = 5;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto b/definitions/code-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto
new file mode 100644
index 000000000..e82bde7dc
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto
@@ -0,0 +1,117 @@
+syntax = "proto3";
+package code.phone.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/phone/v1;phone";
+option java_package = "com.codeinc.gen.phone.v1";
+option objc_class_prefix = "CPBPhoneV1";
+import "common/v1/code_model.proto";
+
+service PhoneVerification {
+ // SendVerificationCode sends a verification code to the provided phone number
+ // over SMS. If an active verification is already taking place, the existing code
+ // will be resent.
+ rpc SendVerificationCode(SendVerificationCodeRequest) returns (SendVerificationCodeResponse);
+ // CheckVerificationCode validates a verification code. On success, a one-time use
+ // token to link an owner account is provided.
+ rpc CheckVerificationCode(CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse);
+ // GetAssociatedPhoneNumber gets the latest verified phone number linked to an owner account.
+ rpc GetAssociatedPhoneNumber(GetAssociatedPhoneNumberRequest) returns (GetAssociatedPhoneNumberResponse);
+}
+message SendVerificationCodeRequest {
+ // The phone number to send a verification code over SMS to.
+ common.v1.PhoneNumber phone_number = 1;
+ // Device token for antispam measures against fake devices
+ common.v1.DeviceToken device_token = 2;
+}
+message SendVerificationCodeResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The phone number is not invited and cannot use Code. The SMS will not
+ // be sent until the user is invited. This result is only valid during
+ // the invitation stage of the application and won't apply after general
+ // public release.
+ NOT_INVITED = 1;
+ // SMS is rate limited (eg. by IP, phone number, etc) and was not sent.
+ // These will be set generously such that real users won't actually hit
+ // the limits.
+ RATE_LIMITED = 2;
+ // The phone number is not real because it fails Twilio lookup.
+ INVALID_PHONE_NUMBER = 3;
+ // The phone number is valid, but it maps to an unsupported type of phone
+ // like a landline or eSIM.
+ UNSUPPORTED_PHONE_TYPE = 4;
+ // The country associated with the phone number is not supported (eg. it
+ // is on the sanctioned list).
+ UNSUPPORTED_COUNTRY = 5;
+ // The device is not supported (eg. it fails device attestation checks)
+ UNSUPPORTED_DEVICE = 6;
+ }
+}
+message CheckVerificationCodeRequest {
+ // The phone number being verified.
+ common.v1.PhoneNumber phone_number = 1;
+ // The verification code received via SMS.
+ VerificationCode code = 2;
+}
+message CheckVerificationCodeResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The provided verification code is invalid. The user may retry
+ // enterring the code if this is received. When max attempts are
+ // received, NO_VERIFICATION will be returned.
+ INVALID_CODE = 1;
+ // There is no verification in progress for the phone number. Several
+ // reasons this can occur include a verification being expired or having
+ // reached a maximum check threshold. The client must initiate a new
+ // verification using SendVerificationCode.
+ NO_VERIFICATION = 2;
+ // The call is rate limited (eg. by IP, phone number, etc). The code is
+ // not verified.
+ RATE_LIMITED = 3;
+ }
+ // The token used to associate an owner account to a user using the verified
+ // phone number.
+ PhoneLinkingToken linking_token = 2;
+}
+message GetAssociatedPhoneNumberRequest {
+ // The public key of the owner account that is being queried for a linked
+ // phone number.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(GetAssociatedPhoneNumberRequest) without
+ // this field set using the private key of owner_account_id. This provides
+ // an authentication mechanism to the RPC.
+ common.v1.Signature signature = 2;
+}
+message GetAssociatedPhoneNumberResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // A phone number is not associated with the provided owner account.
+ NOT_FOUND = 1;
+ // The phone number exists, but is no longer invited
+ NOT_INVITED = 2;
+ // The phone number exists, but at least one timelock account is unlocked
+ UNLOCKED_TIMELOCK_ACCOUNT = 3;
+ }
+ // The latest phone number associated with the owner account.
+ common.v1.PhoneNumber phone_number = 2;
+ // State that determines whether a phone number is linked to the owner
+ // account. A phone number is linked if we can treat it as an alias.
+ // This is notably different from association, which answers the question
+ // of whether the number was linked at any point in time.
+ bool is_linked = 3;
+}
+message VerificationCode {
+ // A 4-10 digit numerical code.
+ string value = 2 ;
+}
+// A one-time use token that can be provided to the Identity service to link an
+// owner account to a user with the verified phone number. The client should
+// treat this token as opaque.
+message PhoneLinkingToken {
+ // The verified phone number.
+ common.v1.PhoneNumber phone_number = 1;
+ // The code that verified the phone number.
+ VerificationCode code = 2;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/push/v1/code_push_service.proto b/definitions/code-vm/protos/src/main/proto/push/v1/code_push_service.proto
new file mode 100644
index 000000000..c054b730a
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/push/v1/code_push_service.proto
@@ -0,0 +1,74 @@
+syntax = "proto3";
+package code.push.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/push/v1;push";
+option java_package = "com.codeinc.gen.push.v1";
+option objc_class_prefix = "APBPushV1";
+import "common/v1/code_model.proto";
+
+service Push {
+ // AddToken stores a push token in a data container. The call is idempotent
+ // and adding an existing valid token will not fail. Token types will be
+ // validated against the user agent and any mismatches will result in an
+ // INVALID_ARGUMENT status error.
+ //
+ // The token will be unlinked from any and all other accounts that it was
+ // previously bound to.
+ rpc AddToken(AddTokenRequest) returns (AddTokenResponse);
+ // RemoveToken removes the provided push token from the account.
+ //
+ // The provided token must be bound to the current account.
+ // Otherwise, the RPC will succeed with without removal.
+ rpc RemoveToken(RemoveTokenRequest) returns (RemoveTokenResponse);
+}
+enum TokenType {
+ UNKNOWN = 0;
+ // FCM registration token for an Android device
+ FCM_ANDROID = 1;
+ // FCM registration token or an iOS device
+ FCM_APNS = 2;
+}
+message AddTokenRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(AddTokenRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The data container where the push token will be stored.
+ common.v1.DataContainerId container_id = 3;
+ // The push token to store
+ string push_token = 4 ;
+ // The type of push token
+ TokenType token_type = 5;
+ // The instance of the app install where the push token was generated. Ideally,
+ // the push token is unique to the install.
+ common.v1.AppInstallId app_install = 6;
+}
+message AddTokenResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The push token is invalid and wasn't stored.
+ INVALID_PUSH_TOKEN = 1;
+ }
+}
+message RemoveTokenRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(AddTokenRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The data container where the push token was stored.
+ common.v1.DataContainerId container_id = 3;
+ // The push token to remove.
+ string push_token = 4 ;
+ // The type of push token to remove.
+ TokenType token_type = 5;
+}
+message RemoveTokenResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
diff --git a/definitions/code-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto b/definitions/code-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto
new file mode 100644
index 000000000..9f5b74883
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto
@@ -0,0 +1,1036 @@
+syntax = "proto3";
+package code.transaction.v2;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2;transaction";
+option java_package = "com.codeinc.gen.transaction.v2";
+option objc_class_prefix = "APBTransactionV2";
+import "common/v1/code_model.proto";
+import "google/protobuf/any.proto";
+import "google/protobuf/timestamp.proto";
+
+service Transaction {
+ // SubmitIntent is the mechanism for client and server to agree upon a set of
+ // client actions to execute on the blockchain using the Code sequencer for
+ // fulfillment.
+ //
+ // Transactions and virtual instructions are never exchanged between client and server.
+ // Instead, the required accounts and arguments for instructions known to each actor are
+ // exchanged to allow independent and local construction.
+ //
+ // Client and server are expected to fully validate the intent. Proofs will
+ // be provided for any parameter requiring one. Signatures should only be
+ // generated after approval.
+ //
+ // This RPC is not a traditional streaming endpoint. It bundles two unary calls
+ // to enable DB-level transaction semantics.
+ //
+ // The high-level happy path flow for the RPC is as follows:
+ // 1. Client initiates a stream and sends SubmitIntentRequest.SubmitActions
+ // 2. Server validates the intent, its actions and metadata
+ // 3a. If there are transactions or virtual instructions requiring the user's signature,
+ // then server returns SubmitIntentResponse.ServerParameters
+ // 3b. Otherwise, server returns SubmitIntentResponse.Success and closes the
+ // stream
+ // 4. For each transaction or virtual instruction requiring the user's signature, the client
+ // locally constructs it, performs validation and collects the signature
+ // 5. Client sends SubmitIntentRequest.SubmitSignatures with the signature
+ // list generated from 4
+ // 6. Server validates all signatures are submitted and are the expected values
+ // using locally constructed transactions or virtual instructions.
+ // 7. Server returns SubmitIntentResponse.Success and closes the stream
+ // In the error case:
+ // * Server will return SubmitIntentResponse.Error and close the stream
+ // * Client will close the stream
+ rpc SubmitIntent(stream SubmitIntentRequest) returns (stream SubmitIntentResponse);
+ // GetIntentMetadata gets basic metadata on an intent. It can also be used
+ // to fetch the status of submitted intents. Metadata exists only for intents
+ // that have been successfully submitted.
+ rpc GetIntentMetadata(GetIntentMetadataRequest) returns (GetIntentMetadataResponse);
+ // GetPrivacyUpgradeStatus gets the status of a private transaction and the
+ // ability to upgrade it to permanent privacy.
+ rpc GetPrivacyUpgradeStatus(GetPrivacyUpgradeStatusRequest) returns (GetPrivacyUpgradeStatusResponse);
+ // GetPrioritizedIntentsForPrivacyUpgrade allows clients to get private
+ // intent actions that can be upgraded in a secure and verifiable manner.
+ rpc GetPrioritizedIntentsForPrivacyUpgrade(GetPrioritizedIntentsForPrivacyUpgradeRequest) returns (GetPrioritizedIntentsForPrivacyUpgradeResponse);
+ // GetLimits gets limits for money moving intents for an owner account in an
+ // identity-aware manner
+ rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse);
+ // CanWithdrawToAccount provides hints to clients for submitting withdraw intents.
+ // The RPC indicates if a withdrawal is possible, and how it should be performed.
+ rpc CanWithdrawToAccount(CanWithdrawToAccountRequest) returns (CanWithdrawToAccountResponse);
+ // Airdrop airdrops Kin to the requesting account
+ rpc Airdrop(AirdropRequest) returns (AirdropResponse);
+ // Swap performs an on-chain swap. The high-level flow mirrors SubmitIntent
+ // closely. However, due to the time-sensitive nature and unreliability of
+ // swaps, they do not fit within the broader intent system. This results in
+ // a few key differences:
+ // * Transactions are submitted on a best-effort basis outside of the Code
+ // Sequencer within the RPC handler
+ // * Balance changes are applied after the transaction has finalized
+ // * Transactions use recent blockhashes over a nonce
+ // SubmitIntent also operates on VM virtual instructions, whereas Swap uses
+ // Solana transactions.
+ //
+ // The transaction will have the following instruction format:
+ // 1. ComputeBudget::SetComputeUnitLimit
+ // 2. ComputeBudget::SetComputeUnitPrice
+ // 3. SwapValidator::PreSwap
+ // 4. Dynamic swap instruction
+ // 5. SwapValidator::PostSwap
+ //
+ // Note: Currently limited to swapping USDC to Kin.
+ // Note: Kin is deposited into the token account derived from the VM deposit PDA of the owner account.
+ rpc Swap(stream SwapRequest) returns (stream SwapResponse);
+ // DeclareFiatOnrampPurchaseAttempt is called whenever a user attempts to use a fiat
+ // onramp to purchase crypto for use in Code.
+ rpc DeclareFiatOnrampPurchaseAttempt(DeclareFiatOnrampPurchaseAttemptRequest) returns (DeclareFiatOnrampPurchaseAttemptResponse);
+}
+//
+// Request and Response Definitions
+//
+message SubmitIntentRequest {
+ oneof request {
+ SubmitActions submit_actions = 1;
+ SubmitSignatures submit_signatures = 2;
+ }
+ message SubmitActions {
+ // The globally unique client generated intent ID. Use the original intent
+ // ID when operating on actions that mutate the intent.
+ common.v1.IntentId id = 1;
+
+ // The verified owner account public key
+ common.v1.SolanaAccountId owner = 2;
+ // Additional metadata that describes the high-level intention
+ Metadata metadata = 3;
+ // The set of all ordered actions required to fulfill the intent
+ repeated Action actions = 4 ;
+ // The signature is of serialize(SubmitActions) without this field set using the
+ // private key of the owner account. This provides an authentication mechanism
+ // to the RPC.
+ common.v1.Signature signature = 5;
+ // Device token for antispam measures against fake devices
+ common.v1.DeviceToken device_token = 6;
+ }
+ message SubmitSignatures {
+ // The set of all signatures for each transaction or virtual instruction requiring
+ // signature from the authority accounts.
+ //
+ // The signature for a transaction is for the marshalled transaction.
+ // The signature for a virtual instruction is the hash of the marshalled instruction.
+ repeated common.v1.Signature signatures = 1 ;
+ }
+}
+message SubmitIntentResponse {
+ oneof response {
+ ServerParameters server_parameters = 1;
+ Success success = 2;
+ Error error = 3;
+ }
+ message ServerParameters {
+ // The set of all server paremeters required to fill missing transaction
+ // or virtual instruction details. Server guarantees to provide a message
+ // for each client action in an order consistent with the received action
+ // list.
+ repeated ServerParameter server_parameters = 1 ;
+ }
+ message Success {
+ Code code = 1;
+ enum Code {
+ // The intent was successfully created and is now scheduled.
+ OK = 0;
+ }
+ // todo: Revisit if we need side-effects. Clients are effecitively doing
+ // local simulation now with the privacy solution.
+ }
+ message Error {
+ Code code = 1;
+ enum Code {
+ // Denied by a guard (spam, money laundering, etc)
+ DENIED = 0;
+ // The intent is invalid.
+ INVALID_INTENT = 1;
+ // There is an issue with provided signatures.
+ SIGNATURE_ERROR = 2;
+ // Server detected client has stale state.
+ STALE_STATE = 3;
+ }
+ repeated ErrorDetails error_details = 2;
+ }
+}
+message GetIntentMetadataRequest {
+ // The intent ID to query
+ common.v1.IntentId intent_id = 1;
+ // The verified owner account public key when not signing with the rendezvous
+ // key. Only owner accounts involved in the intent can access the metadata.
+ common.v1.SolanaAccountId owner = 2;
+ // The signature is of serialize(GetIntentStatusRequest) without this field set
+ // using the private key of the rendezvous or owner account. This provides an
+ // authentication mechanism to the RPC.
+ common.v1.Signature signature = 3;
+}
+message GetIntentMetadataResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ Metadata metadata = 2;
+}
+message GetPrivacyUpgradeStatusRequest {
+ // The intent ID
+ common.v1.IntentId intent_id = 1;
+ // The action ID for private transfer
+ uint32 action_id = 2;
+}
+message GetPrivacyUpgradeStatusResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The provided intent ID doesn't exist
+ INTENT_NOT_FOUND = 1;
+ // The provided action ID doesn't exist
+ ACTION_NOT_FOUND = 2;
+ // The provided action doesn't map to a private transfer
+ INVALID_ACTION = 3;
+ }
+ Status status = 2;
+ enum Status {
+ UNKNOWN = 0;
+ // The action for the temporary private transfer was submitted and
+ // finalized. The opportunity to upgrade was missed.
+ TEMPORARY_ACTION_FINALIZED = 1;
+ // The next block of private transfers hasn't been created, so there
+ // is no proof available. Wait and try again later.
+ WAITING_FOR_NEXT_BLOCK = 2;
+ // The action can be upgraded to permanent privacy
+ READY_FOR_UPGRADE = 3;
+ // The action has already been upgraded
+ ALREADY_UPGRADED = 4;
+ }
+}
+message GetPrioritizedIntentsForPrivacyUpgradeRequest {
+ // The owner account to query against for upgradeable intents.
+ common.v1.SolanaAccountId owner = 1;
+ // The maximum number of intents to return in the response. Default is 10.
+ uint32 limit = 2;
+ // The signature is of serialize(GetPrioritizedIntentsForPrivacyUpgradeRequest)
+ // without this field set using the private key of the owner account. This
+ // provides an authentication mechanism to the RPC.
+ common.v1.Signature signature = 3;
+}
+message GetPrioritizedIntentsForPrivacyUpgradeResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ // Ordered from highest to lowest priority
+ repeated UpgradeableIntent items = 2 ;
+}
+message GetLimitsRequest {
+ // The owner account whose limits will be calculated. Any other owner accounts
+ // linked with the same identity of the owner will also be applied.
+ common.v1.SolanaAccountId owner = 1;
+ // The signature is of serialize(GetLimitsRequest) without this field set
+ // using the private key of the owner account. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // All transactions starting at this time will be incorporated into the consumed
+ // limit calculation. Clients should set this to the start of the current day in
+ // the client's current time zone (because server has no knowledge of this atm).
+ google.protobuf.Timestamp consumed_since = 3;
+}
+message GetLimitsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ // Send limits keyed by currency
+ map send_limits_by_currency = 2;
+ // Deposit limits
+ DepositLimit deposit_limit = 3;
+ // Micro payment limits keyed by currency
+ map micro_payment_limits_by_currency = 4;
+ // Buy module limits keyed by currency
+ map buy_module_limits_by_currency = 5;
+}
+message CanWithdrawToAccountRequest {
+ common.v1.SolanaAccountId account = 1;
+}
+message CanWithdrawToAccountResponse {
+ // Metadata so the client knows how to withdraw to the account. Server cannot
+ // provide precalculated addresses in this response to maintain non-custodial
+ // status.
+ AccountType account_type = 2;
+ enum AccountType {
+ Unknown = 0; // Server cannot determine
+ TokenAccount = 1; // Client uses the address as is in SubmitIntent
+ OwnerAccount = 2; // Client locally derives the ATA to use in SubmitIntent
+ }
+ // Server-controlled flag to indicate if the account can be withdrawn to.
+ // There are several reasons server may deny it, including:
+ // - Wrong type of Code account
+ // - Not wanting to subsidize the creation of an ATA
+ // - Unsupported external account type (eg. token account but of the wrong mint)
+ // This is guaranteed to be false when account_type = Unknown.
+ bool is_valid_payment_destination = 1;
+ // Token account requires initialization before the withdrawal can occur.
+ // Server has chosen not to subsidize the fees. The response is guaranteed
+ // to have set is_valid_payment_destination = false in this case.
+ bool requires_initialization = 3;
+}
+message AirdropRequest {
+ // The type of airdrop to claim
+ AirdropType airdrop_type = 1 ;
+ // The owner account to airdrop Kin to
+ common.v1.SolanaAccountId owner = 2;
+ // The signature is of serialize(AirdropRequest) without this field set
+ // using the private key of the owner account. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 3;
+}
+message AirdropResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // Airdrops are unavailable
+ UNAVAILABLE = 1;
+ // The airdrop has already been claimed by the owner
+ ALREADY_CLAIMED = 2;
+ }
+ // Exchange data for the amount of Kin airdropped when successful
+ ExchangeData exchange_data = 2;
+}
+message SwapRequest {
+ oneof request {
+ Initiate initiate = 1;
+ SubmitSignature submit_signature = 2;
+ }
+ message Initiate {
+ // The verified owner account public key
+ common.v1.SolanaAccountId owner = 1;
+ // The user authority account that will sign to authorize the swap. Ideally,
+ // this is an account derived off the owner account that is solely responsible
+ // for swapping.
+ common.v1.SolanaAccountId swap_authority = 2;
+ // Maximum amount to swap from the source mint, in quarks. If value is set to zero,
+ // the entire amount will be swapped.
+ uint64 limit = 3;
+ // Whether the client wants the RPC to wait for blockchain status. If false,
+ // then the RPC will return Success when the swap is submitted to the blockchain.
+ // Otherwise, the RPC will observe and report back the status of the transaction.
+ bool wait_for_blockchain_status = 4;
+ // The signature is of serialize(Initiate) without this field set using the
+ // private key of the owner account. This provides an authentication mechanism
+ // to the RPC.
+ common.v1.Signature signature = 5;
+ }
+ message SubmitSignature {
+ // The signature for the locally constructed swap transaction
+ common.v1.Signature signature = 1;
+ }
+}
+message SwapResponse {
+ oneof response {
+ ServerParameters server_parameters = 1;
+ Success success = 2;
+ Error error = 3;
+ }
+ message ServerParameters {
+ // Subisdizer account that will be paying for the swap
+ common.v1.SolanaAccountId payer = 1;
+ // Recent blockhash
+ common.v1.Blockhash recent_blockhash = 2;
+ // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit
+ // instruction. If the value is 0, then the instruction can be omitted.
+ uint32 compute_unit_limit = 3;
+ // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice
+ // instruction. If the value is 0, then the instruction can be omitted.
+ uint64 compute_unit_price = 4;
+ // On-chain program that will be performing the swap
+ common.v1.SolanaAccountId swap_program = 5;
+ // Accounts provided to the swap instruction
+ repeated common.v1.InstructionAccount swap_ixn_accounts = 6 ;
+ // Instruction data for the swap instruction
+ bytes swap_ixn_data = 7 ;
+ // Maximum quarks that will be sent out of the source account after
+ // executing the swap. If not, the validation instruction will cause
+ // the transaction to fail.
+ uint64 max_to_send = 8;
+ // Minimum quarks that will be received into the destination account
+ // after executing the swap. If not, the validation instruction will
+ // cause the transaction to fail.
+ uint64 min_to_receive = 9;
+ // Nonce to use in swap validator state account PDA
+ common.v1.SolanaAccountId nonce = 10;
+ }
+ message Success {
+ Code code = 1;
+ enum Code {
+ // The swap was submitted to the blockchain.
+ SWAP_SUBMITTED = 0;
+ // The swap was finalized on the blockchain.
+ SWAP_FINALIZED = 1;
+ }
+ }
+ message Error {
+ Code code = 1;
+ enum Code {
+ // Denied by a guard (spam, money laundering, etc)
+ DENIED = 0;
+ // There is an issue with the provided signature.
+ SIGNATURE_ERROR = 2;
+ // The swap failed server-side validation
+ INVALID_SWAP = 3;
+ // The submitted swap transaction failed. Attempt the swap again.
+ SWAP_FAILED = 4;
+ }
+ repeated ErrorDetails error_details = 2;
+ }
+}
+message DeclareFiatOnrampPurchaseAttemptRequest {
+ // The owner account invoking the buy module
+ common.v1.SolanaAccountId owner = 1;
+ // The amount being purchased
+ ExchangeDataWithoutRate purchase_amount = 2;
+ // A nonce value unique to the purchase. If it's included in a memo for the
+ // transaction for the deposit to the owner, then purchase_amount will be used
+ // for display values. Otherwise, the amount will be inferred from the transaction.
+ common.v1.UUID nonce = 3;
+ // The signature is of serialize(DeclareFiatOnrampPurchaseAttemptRequest) without
+ // this field set using the private key of the owner account. This provides an
+ // authentication mechanism to the RPC.
+ common.v1.Signature signature = 4;
+}
+message DeclareFiatOnrampPurchaseAttemptResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The owner account is not valid (ie. it isn't a Code account)
+ INVALID_OWNER = 1;
+ // The currency isn't supported
+ UNSUPPORTED_CURRENCY = 2;
+ // The amount specified exceeds limits
+ AMOUNT_EXCEEDS_MAXIMUM = 3;
+ }
+}
+//
+// Metadata definitions
+//
+// Metadata describes the high-level details of an intent
+message Metadata {
+ oneof type {
+ OpenAccountsMetadata open_accounts = 1;
+ SendPrivatePaymentMetadata send_private_payment = 2;
+ ReceivePaymentsPrivatelyMetadata receive_payments_privately = 3;
+ UpgradePrivacyMetadata upgrade_privacy = 4;
+ SendPublicPaymentMetadata send_public_payment = 6;
+ ReceivePaymentsPubliclyMetadata receive_payments_publicly = 7;
+ EstablishRelationshipMetadata establish_relationship = 8;
+ }
+ reserved 5; // Deprecated MigrateToPrivacy2022Metadata
+}
+// ExtendedPaymentMetadata is additional metadata that can be used for custom
+// payment types
+message ExtendedPaymentMetadata {
+ google.protobuf.Any value = 1;
+;
+}
+// Open a set of accounts. Currently, clients should only use this for new users
+// to open all required accounts up front (buckets, incoming, and outgoing).
+//
+// Action Spec:
+//
+// for account in [PRIMARY, TEMPORARY_INCOMING, TEMPORARY_OUTGOING, BUCKET_1_KIN, ... , BUCKET_1_000_000_KIN]
+// actions.push_back(OpenAccountAction(account))
+message OpenAccountsMetadata {
+ // Nothing is currently required
+}
+// Sends a payment to a destination account with initial temporary privacy. Clients
+// should also reorganize their bucket accounts and rotate their temporary outgoing
+// account.
+//
+// Action Spec (In Person Cash Payment or Withdrawal or Tip):
+//
+// actions = [
+// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations
+//
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+// ...,
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+//
+// // Section 2: Rotate TEMPORARY_OUTGOING account
+//
+// // Below must appear last in this exact order
+// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks),
+// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]),
+// ]
+//
+// Action Spec (Remote Send):
+//
+// actions = [
+// // Section 1: Open REMOTE_SEND_GIFT_CARD account
+//
+// OpenAccountAction(REMOTE_SEND_GIFT_CARD),
+//
+// // Section 2: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations
+//
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+// ...,
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+//
+// // Section 3: Rotate TEMPORARY_OUTGOING account
+//
+// // Below must appear last in this exact order
+// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], REMOTE_SEND_GIFT_CARD, ExchangeData.Quarks),
+// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]),
+//
+// // Section 4: Close REMOTE_SEND_GIFT_CARD if not redeemed after period of time
+//
+// todo: We need a new mechanism that doesn't rely on the deprecated CloseDormantAccountAction
+//
+// Action Spec (Micro Payment):
+//
+// actions = [
+// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations
+//
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+// ...,
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize),
+//
+// // Section 2: Fee payments
+//
+// // Hard-coded Code $0.01 USD fee to a dynamic fee account
+// FeePayment(TEMPORARY_OUTGOING[index], codeFeeAccount, $0.01 USD of Kin),
+//
+// // Additional fees, exactly as specified in the original payment request
+// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccount0, additionalFeeQuarks0),
+// ...
+// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccountN, additionalFeeQuarksN),
+//
+// // Section 3: Rotate TEMPORARY_OUTGOING account
+//
+// // Below must appear last in this exact order
+// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks - $0.01 USD of Kin - additionalFeeQuarks0 - ... - additionalFeeQuarksN),
+// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]),
+// ]
+message SendPrivatePaymentMetadata {
+ // The destination token account to send funds to
+ common.v1.SolanaAccountId destination = 1;
+ // The exchange data of total funds being sent to the destination
+ ExchangeData exchange_data = 2;
+ // Is the payment a withdrawal? For destinations that are not Code temporary
+ // accounts, this must be set to true.
+ bool is_withdrawal = 3;
+ // Is the payment for a remote send?
+ bool is_remote_send = 4;
+ // Is the payment for a tip?
+ bool is_tip = 5;
+ // If is_tip is true, the user being tipped
+ TippedUser tipped_user = 6;
+}
+// Send a payment to a destination account publicly.
+//
+// Action Spec:
+//
+// source = PRIMARY or RELATIONSHIP
+// actions = [NoPrivacyTransferAction(source, destination, ExchangeData.Quarks)]
+message SendPublicPaymentMetadata {
+ // The primary or relatinship account where funds will be sent from. The primary
+ // account is assumed if this field is not set for backwards compatibility with
+ // old clients.
+ common.v1.SolanaAccountId source = 4;
+ // The destination token account to send funds to. This cannot be a Code
+ // temporary account.
+ common.v1.SolanaAccountId destination = 1;
+ // The exchange data of total funds being sent to the destination
+ ExchangeData exchange_data = 2;
+ // Is the payment a withdrawal? Currently, this is always true.
+ bool is_withdrawal = 3;
+ ExtendedPaymentMetadata extended_metadata = 5;
+}
+// Receive funds into an organizer with initial temporary privacy. Clients should
+// also reorganize their bucket accounts and rotate their temporary incoming account
+// as applicable. Only accounts owned and derived by a user's 12 words should operate
+// as a source in this intent type to guarantee privacy upgradeability.
+//
+// Action Spec (Payment):
+//
+// actions = [
+// // Section 1: Transfer Quarks from TEMPORARY_INCOMING account to BUCKET_X_KIN accounts with reorganizations
+//
+// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// ...,
+// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+//
+// // Section 2: Rotate TEMPORARY_INCOMING account
+//
+// // Below must appear last in this exact order
+// OpenAccountAction(TEMPORARY_INCOMING[index + 1])
+// ]
+//
+// Action Spec (Deposit):
+//
+// source = PRIMARY or RELATIONSHIP
+// actions = [
+// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// ...,
+// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize),
+// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize),
+// ]
+message ReceivePaymentsPrivatelyMetadata {
+ // The temporary incoming, primary or relationship account to receive funds from
+ common.v1.SolanaAccountId source = 1;
+ // The exact amount of Kin in quarks being received
+ uint64 quarks = 2;
+ // Is the receipt of funds from a deposit? If true, the source account must
+ // be a primary or relationship account. Otherwise, it must be from a temporary
+ // incoming account.
+ bool is_deposit = 3;
+}
+// Receive funds into a user-owned account publicly. All use cases of this intent
+// close the account, so all funds must be moved. Use this intent to receive payments
+// from an account not owned by a user's 12 words into a temporary incoming account,
+// which will guarantee privacy upgradeability.
+//
+// Action Spec (Remote Send):
+//
+// actions = [NoPrivacyWithdrawAction(REMOTE_SEND_GIFT_CARD, TEMPORARY_INCOMING[latest_index], quarks)]
+message ReceivePaymentsPubliclyMetadata {
+ // The remote send gift card to receive funds from
+ common.v1.SolanaAccountId source = 1;
+ // The exact amount of Kin in quarks being received
+ uint64 quarks = 2;
+ // Is the receipt of funds from a remote send gift card? Currently, this is
+ // the only use case for this intent and validation enforces the flag to true.
+ bool is_remote_send = 3;
+ // If is_remote_send is true, is the gift card being voided? The user owner
+ // account's 12 words that issued the gift card may only set this flag to true.
+ // Functionally, this doesn't affect the intent, but rather if we decide to show
+ // it in a user-friendly payment history.
+ bool is_issuer_voiding_gift_card = 4;
+ // If is_remote_send is true, the original exchange data that was provided as
+ // part of creating the gift card account. This is purely a server-provided value.
+ // SubmitIntent will disallow this being set.
+ ExchangeData exchange_data = 5;
+}
+// Upgrade existing private virtual instructions from temporary to permanent privacy.
+message UpgradePrivacyMetadata {
+ // Nothing is currently required
+}
+// Establishes a long-lived private relationship between a user and another
+// entity.
+//
+// Prereqs:
+// - OpenAccounts intent has been submitted
+//
+// Action spec:
+//
+// actions = [OpenAccountAction(RELATIONSHIP)]
+message EstablishRelationshipMetadata {
+ common.v1.Relationship relationship = 1;
+}
+//
+// Action Definitions
+//
+// Action is a well-defined, ordered and small set of transactions or virtual instructions
+// for a unit of work that the client wants to perform on the blockchain. Clients provide
+// parameters known to them in the action.
+message Action {
+ // The ID of this action, which is unique within an intent. It must match
+ // the index of the action's location in the SubmitAction's actions field.
+ uint32 id = 1;
+ // The type of action to perform.
+ oneof type {
+ OpenAccountAction open_account = 2;
+ NoPrivacyTransferAction no_privacy_transfer = 5;
+ NoPrivacyWithdrawAction no_privacy_withdraw = 6;
+ TemporaryPrivacyTransferAction temporary_privacy_transfer = 7;
+ TemporaryPrivacyExchangeAction temporary_privacy_exchange = 8;
+ PermanentPrivacyUpgradeAction permanent_privacy_upgrade = 9;
+ FeePaymentAction fee_payment = 10;
+ }
+ reserved 3; // Deprecated CloseEmptyAccountAction
+ reserved 4; // Deprecated CloseDormantAccountAction
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. cvm::SystemTimelockInit
+// Client Signature Required: No
+message OpenAccountAction {
+ // The type of account, which will dictate its intended use
+ common.v1.AccountType account_type = 1;
+ // The owner of the account. For accounts liked to a user's 12 words, this is
+ // the verified parent owner account public key. All other account types should
+ // set this to the authority value.
+ common.v1.SolanaAccountId owner = 2;
+ // The index used to for accounts that are derived from owner
+ uint64 index = 3;
+ // The public key of the private key that has authority over the opened token account
+ common.v1.SolanaAccountId authority = 4;
+ // The token account being opened
+ common.v1.SolanaAccountId token = 5;
+ // The signature is of serialize(OpenAccountAction) without this field set
+ // using the private key of the authority account. This provides a proof
+ // of authorization to link authority to owner.
+ common.v1.Signature authority_signature = 6;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::TransferWithAuthority (source -> destination)
+// Client Signature Required: Yes
+message NoPrivacyTransferAction {
+ // The public key of the private key that has authority over source
+ common.v1.SolanaAccountId authority = 1;
+ // The source account where funds are transferred from
+ common.v1.SolanaAccountId source = 2;
+ // The destination account where funds are transferred to
+ common.v1.SolanaAccountId destination = 3;
+ // The Kin quark amount to transfer
+ uint64 amount = 4;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::RevokeLockWithAuthority
+// 4. timelock::DeactivateLock
+// 5. timelock::Withdraw (source -> destination)
+// 6. timelock::CloseAccounts
+// Client Signature Required: Yes
+message NoPrivacyWithdrawAction {
+ // The public key of the private key that has authority over source
+ common.v1.SolanaAccountId authority = 1;
+ // The source account where funds are transferred from
+ common.v1.SolanaAccountId source = 2;
+ // The destination account where funds are transferred to
+ common.v1.SolanaAccountId destination = 3;
+ // The intended Kin quark amount to withdraw
+ uint64 amount = 4;
+
+ // Whether the account is closed afterwards. This is always true, since there
+ // are no current se cases to leave it open.
+ bool should_close = 5;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. splitter::TransferWithCommitment (treasury -> destination)
+// Client Signature Required: No
+//
+// Virtual Instruction 2
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::TransferWithAuthority (source -> commitment)
+// Client Signature Required: Yes
+message TemporaryPrivacyTransferAction {
+ // The public key of the private key that has authority over source
+ common.v1.SolanaAccountId authority = 1;
+ // The source account where funds are transferred from
+ common.v1.SolanaAccountId source = 2;
+ // The destination account where funds are transferred to
+ common.v1.SolanaAccountId destination = 3;
+ // The Kin quark amount to transfer
+ uint64 amount = 4;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. splitter::TransferWithCommitment (treasury -> destination)
+// Client Signature Required: No
+//
+// Virtual Instruction 2
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::TransferWithAuthority (source -> commitment)
+// Client Signature Required: Yes
+message TemporaryPrivacyExchangeAction {
+ // The public key of the private key that has authority over source
+ common.v1.SolanaAccountId authority = 1;
+ // The source account where funds are exchanged from
+ common.v1.SolanaAccountId source = 2;
+ // The destination account where funds are exchanged to
+ common.v1.SolanaAccountId destination = 3;
+ // The Kin quark amount to exchange
+ uint64 amount = 4;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::TransferWithAuthority (source -> different commitment)
+// Client Signature Required: Yes
+message PermanentPrivacyUpgradeAction {
+ // The action ID of the temporary private transfer or exchange to upgrade
+ uint32 action_id = 1;
+}
+// Virtual Instruction 1
+// Instructions:
+// 1. system::AdvanceNonce
+// 2. memo::Memo
+// 3. timelock::TransferWithAuthority (source -> fee account)
+// Client Signature Required: Yes
+//
+// Note: This is exactly a NoPrivacyTransferAction, but with specialized metadata
+// for fees.
+message FeePaymentAction {
+ // The type of fee being operated on
+ FeeType type = 4;
+ enum FeeType {
+ CODE = 0; // Hardcoded $0.01 USD fee to a dynamic fee account specified by server
+ THIRD_PARTY = 1; // Third party fee specified at time of payment request
+ }
+ // The public key of the private key that has authority over source
+ common.v1.SolanaAccountId authority = 1;
+ // The source account where funds are transferred from
+ common.v1.SolanaAccountId source = 2;
+ // The Kin quark amount to transfer
+ uint64 amount = 3;
+ // The destination where the fee payment is being made for fees outside of
+ // Code.
+ common.v1.SolanaAccountId destination = 5;
+}
+//
+// Server Parameter Definitions
+//
+// ServerParameter are a set of parameters known and returned by server that
+// enables clients to complete transaction construction. Any necessary proofs,
+// which are required to be locally verifiable, are also provided to ensure
+// safe use in the event of a malicious server.
+message ServerParameter {
+ // The action the server parameters belong to
+ uint32 action_id = 1;
+ // The set of nonces used for the action. Server will only provide values
+ // for transactions requiring client signatures.
+ repeated NoncedTransactionMetadata nonces = 2 ;
+ // The type of server parameter which maps to the type of action requested
+ oneof type {
+ OpenAccountServerParameter open_account = 3;
+ NoPrivacyTransferServerParameter no_privacy_transfer = 6;
+ NoPrivacyWithdrawServerParameter no_privacy_withdraw = 7;
+ TemporaryPrivacyTransferServerParameter temporary_privacy_transfer = 8;
+ TemporaryPrivacyExchangeServerParameter temporary_privacy_exchange = 9;
+ PermanentPrivacyUpgradeServerParameter permanent_privacy_upgrade = 10;
+ FeePaymentServerParameter fee_payment = 11;
+ }
+ reserved 4; // Deprecated CloseEmptyAccountServerParameter
+ reserved 5; // Deprecated CloseDormantAccountServerParameter
+}
+// For transactions, the nonce is a standard nonce on Solana
+// For virtual instructions, the nonce is a virtual nonce on the Code VM
+message NoncedTransactionMetadata {
+ // The nonce account to use in the system::AdvanceNonce instruction
+ common.v1.SolanaAccountId nonce = 1;
+ // The blockhash to set in the transaction or virtual instruction
+ common.v1.Blockhash blockhash = 2;
+}
+message OpenAccountServerParameter {
+ // There are no transactions requiring client signatures
+}
+message NoPrivacyTransferServerParameter {
+ // There are no action-specific server parameters
+}
+message NoPrivacyWithdrawServerParameter {
+ // There are no action-specific server parameters
+}
+message TemporaryPrivacyTransferServerParameter {
+ // The treasury that will be used to split payments and provide a level of privacy
+ common.v1.SolanaAccountId treasury = 1;
+ // A recent root server observed from the treasury
+ common.v1.Hash recent_root = 2;
+}
+message TemporaryPrivacyExchangeServerParameter {
+ // The treasury that will be used to split payments and provide a level of privacy
+ common.v1.SolanaAccountId treasury = 1;
+ // A recent root server observed from the treasury
+ common.v1.Hash recent_root = 2;
+}
+message PermanentPrivacyUpgradeServerParameter {
+ // The new commitment that is being paid
+ common.v1.SolanaAccountId new_commitment = 1;
+ // The new commitment account's transcript. This is purely needed by client
+ // to validate merkle_root with commitment PDA logic.
+ common.v1.Hash new_commitment_transcript = 2;
+ // The new commitment account's destination. This is purely needed by client
+ // to validate merkle_root with commitment PDA logic.
+ common.v1.SolanaAccountId new_commitment_destination = 3;
+ // The new commitment account's payment amount. This is purely needed by client
+ // to validate merkle_root with commitment PDA logic.
+ uint64 new_commitment_amount = 4;
+ // The merkle root, which was the recent root used in the new commitment account
+ common.v1.Hash merkle_root = 5;
+ // The merkle proof that validates the original commitment occurred prior to
+ // the new commitment server is asking client to pay
+ repeated common.v1.Hash merkle_proof = 6 ;
+}
+message FeePaymentServerParameter {
+ // The destination account where Code fee payments should be sent. This will
+ // only be set when the corresponding FeePaymentAction Type is CODE.
+ common.v1.SolanaAccountId code_destination = 1;
+}
+//
+// Structured Error Definitions
+//
+message ErrorDetails {
+ oneof type {
+ ReasonStringErrorDetails reason_string = 1;
+ InvalidSignatureErrorDetails invalid_signature = 2;
+ DeniedErrorDetails denied = 3;
+ }
+}
+message ReasonStringErrorDetails {
+ // Human readable string indicating the failure.
+ string reason = 1 ;
+}
+message InvalidSignatureErrorDetails {
+ // The action whose signature mismatched
+ uint32 action_id = 1;
+ oneof expected_blob {
+ // The transaction the server expected to have signed.
+ common.v1.Transaction expected_transaction = 2;
+ // The virtual ixn hash the server expected to have signed.
+ common.v1.Hash expected_vixn_hash = 4;
+ }
+ // The signature that was provided by the client.
+ common.v1.Signature provided_signature = 3;
+}
+message DeniedErrorDetails {
+ Code code = 1;
+ enum Code {
+ // Reason code not yet defined
+ UNSPECIFIED = 0;
+ // Phone number has exceeded its free account allocation
+ TOO_MANY_FREE_ACCOUNTS_FOR_PHONE_NUMBER = 1;
+ // Device has exceeded its free account allocation
+ TOO_MANY_FREE_ACCOUNTS_FOR_DEVICE = 2;
+ // The country associated with the phone number with the account is not
+ // supported (eg. it is on the sanctioned list).
+ UNSUPPORTED_COUNTRY = 3;
+ // The device is not supported (eg. it fails device attestation checks)
+ UNSUPPORTED_DEVICE = 4;
+ }
+ // Human readable string indicating the failure.
+ string reason = 2 ;
+}
+//
+// Other Model Definitions
+//
+// UpgradeableIntent is an intent whose actions can be upgraded.
+message UpgradeableIntent {
+ // The intent ID
+ common.v1.IntentId id = 1;
+ // The set of private actions that can be upgraded
+ repeated UpgradeablePrivateAction actions = 2 ;
+ message UpgradeablePrivateAction {
+ // The blob that was hashed and signed by the client for a virtual instruction.
+ // Clients *MUST* use the source and destination accounts in the timelock::TransferWithAuthority
+ // instruction to validate all fields provided by server by locally computing the expected
+ // addresses.
+ common.v1.Transaction transaction_blob = 1;
+ // The client's signature for the virtual instruction. Clients MUST use this to
+ // locally validate the virtual instruction blob provided by server.
+ common.v1.Signature client_signature = 2;
+ // The action ID of this virtual instruction
+ uint32 action_id = 3;
+ // The source account's type, which hints how to efficiently derive source
+ common.v1.AccountType source_account_type = 4;
+;
+ // The source account's derivation index, which hints how to efficiently derive source
+ uint64 source_derivation_index = 5;
+ // The original destination account that was paid by the treasury
+ common.v1.SolanaAccountId original_destination = 6;
+ // The original quark amount for the action
+ uint64 original_amount = 7;
+ // The treasury used for this the private action
+ common.v1.SolanaAccountId treasury = 8;
+ // The recent root observed at the time of intent creation for this private action
+ common.v1.Hash recent_root = 9;
+ }
+}
+// ExchangeData defines an amount of Kin with currency exchange data
+message ExchangeData {
+ // ISO 4217 alpha-3 currency code.
+ string currency = 1;
+ // The agreed upon exchange rate. This might not be the same as the
+ // actual exchange rate at the time of intent or fund transfer.
+ double exchange_rate = 2;
+ // The agreed upon transfer amount in the currency the payment was made
+ // in.
+ double native_amount = 3;
+ // The exact amount of quarks to send. This will be used as the source of
+ // truth for validating transaction transfer amounts.
+ uint64 quarks = 4;
+}
+message ExchangeDataWithoutRate {
+ // ISO 4217 alpha-3 currency code.
+ string currency = 1;
+ // The agreed upon transfer amount in the currency the payment was made
+ // in.
+ double native_amount = 2;
+}
+message AdditionalFeePayment {
+ // Destination Kin token account where the fee payment will be made
+ common.v1.SolanaAccountId destination = 1;
+ // Fee percentage, in basis points, of the total quark amount of a payment.
+ uint32 fee_bps = 2 ;
+}
+message SendLimit {
+ // Remaining limit to apply on the next transaction
+ float next_transaction = 1;
+ // Maximum allowed on a per-transaction basis
+ float max_per_transaction = 2;
+ // Maximum allowed on a per-day basis
+ float max_per_day = 3;
+}
+message DepositLimit {
+ // Maximum quarks that may be deposited at any time. Server will guarantee
+ // this threshold will be below enforced dollar value limits, while also
+ // ensuring sufficient funds are available for a full organizer that supports
+ // max payment sends. Total dollar value limits may be spread across many deposits.
+ uint64 max_quarks = 1;
+}
+message MicroPaymentLimit {
+ // Maximum native amount that can be applied per micro payment transaction
+ float max_per_transaction = 1;
+ // Minimum native amount that can be applied per micro payment transaction
+ float min_per_transaction = 2;
+}
+message BuyModuleLimit {
+ // Minimum amount that can be purchased through the buy module
+ float min_per_transaction = 1;
+ // Maximum amount that can be purchased through the buy module
+ float max_per_transaction = 2;
+}
+message TippedUser {
+ Platform platform = 1 ;
+ enum Platform {
+ UNKNOWN = 0;
+ TWITTER = 1;
+ }
+ string username = 2 ;
+}
+message Cursor {
+ bytes value = 1 ;
+}
+enum AirdropType {
+ UNKNOWN = 0;
+ // Reward for giving someone else their first Kin
+ GIVE_FIRST_KIN = 1;
+ // Airdrop for getting a user started with first Kin balance
+ GET_FIRST_KIN = 2;
+}
diff --git a/definitions/code-vm/protos/src/main/proto/user/v1/code_identity_service.proto b/definitions/code-vm/protos/src/main/proto/user/v1/code_identity_service.proto
new file mode 100644
index 000000000..4c71a3ae1
--- /dev/null
+++ b/definitions/code-vm/protos/src/main/proto/user/v1/code_identity_service.proto
@@ -0,0 +1,294 @@
+syntax = "proto3";
+package code.user.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/user/v1;user";
+option java_package = "com.codeinc.gen.user.v1";
+option objc_class_prefix = "CPBUserV1";
+import "common/v1/code_model.proto";
+import "phone/v1/code_phone_verification_service.proto";
+import "transaction/v2/code_transaction_service.proto";
+
+service Identity {
+ // LinkAccount links an owner account to the user identified and authenticated
+ // by a one-time use token.
+ //
+ // Notably, this RPC has the following side effects:
+ // * A new user is automatically created if one doesn't exist.
+ // * Server will create a new data container for at least every unique
+ // owner account linked to the user.
+ rpc LinkAccount(LinkAccountRequest) returns (LinkAccountResponse);
+ // UnlinkAccount removes links from an owner account. It will NOT remove
+ // existing associations between users, owner accounts and identifying
+ // features.
+ //
+ // The following associations will remain intact to ensure owner accounts
+ // can continue to be used with a consistent login experience:
+ // * the user continues to be associated to existing owner accounts and
+ // identifying features
+ //
+ // Client can continue mainting their current login session. Their current
+ // user and data container will remain the same.
+ //
+ // The call is guaranteed to be idempotent. It will not fail if the link is
+ // already removed by either a previous call to this RPC or by a more recent
+ // call to LinkAccount. A failure will only occur if the link between a user
+ // and the owner accout or identifying feature never existed.
+ rpc UnlinkAccount(UnlinkAccountRequest) returns (UnlinkAccountResponse);
+ // GetUser gets user information given a user identifier and an owner account.
+ rpc GetUser(GetUserRequest) returns (GetUserResponse);
+ // UpdatePreferences updates user preferences.
+ rpc UpdatePreferences(UpdatePreferencesRequest) returns (UpdatePreferencesResponse);
+ // LoginToThirdPartyApp logs a user into a third party app for a given intent
+ // ID. If the original request requires payment, then SubmitIntent must be called.
+ rpc LoginToThirdPartyApp(LoginToThirdPartyAppRequest) returns (LoginToThirdPartyAppResponse);
+ // GetLoginForThirdPartyApp gets a login for a third party app from an existing
+ // request. This endpoint supports all paths where login is possible (login on payment,
+ // raw login, etc.).
+ rpc GetLoginForThirdPartyApp(GetLoginForThirdPartyAppRequest) returns (GetLoginForThirdPartyAppResponse);
+ // GetTwitterUser gets Twitter user information
+ //
+ // Note 1: This RPC will only return results for Twitter users that have
+ // accounts linked with Code.
+ //
+ // Note 2: This RPC is heavily cached, and may not reflect real-time Twitter
+ // information.
+ rpc GetTwitterUser(GetTwitterUserRequest) returns (GetTwitterUserResponse);
+}
+message LinkAccountRequest {
+ // The public key of the owner account that will be linked to a user.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(LinkAccountRequest) without this field set
+ // using the private key of owner_account_id. This validates that the client
+ // actually owns the account.
+ common.v1.Signature signature = 2;
+ // A one-time use token that identifies and authenticates the user.
+ oneof token {
+ // A token received after successfully verifying a phone number via a
+ // SMS code using the phone verification service.
+ phone.v1.PhoneLinkingToken phone = 3;
+ }
+}
+message LinkAccountResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The provided token is invalid. A token may be invalid for a number of
+ // reasons including: it's already been used, has been modified by the
+ // client or has expired.
+ INVALID_TOKEN = 1;
+ // The client is rate limited (eg. by IP, user ID, etc). The client should
+ // retry at a later time.
+ RATE_LIMITED = 2;
+ }
+ // The user that was linked to the owner account
+ User user = 2;
+ // The data container where the user can store a copy of their data
+ common.v1.DataContainerId data_container_id = 3;
+ // Field 4 is the deprecated kin_token_account_details
+ reserved 4;
+ // Metadata about the user based for the instance of their view
+ oneof metadata {
+ // Metadata that corresponds to a phone-based identifying feature.
+ PhoneMetadata phone = 5;
+ }
+}
+message UnlinkAccountRequest {
+ // The public key of the owner account that will be unliked.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(UnlinkAccountRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ oneof identifying_feature {
+ // The phone number associated with the owner account.
+ common.v1.PhoneNumber phone_number = 4;
+ }
+}
+message UnlinkAccountResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The client attempted to unlink an owner account or identifying feature
+ // that never had a valid association.
+ NEVER_ASSOCIATED = 1;
+ }
+}
+message GetUserRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The signature is of serialize(GetUserRequest) without this field set
+ // using the private key of owner_account_id. This provides an authentication
+ // mechanism to the RPC.
+ common.v1.Signature signature = 2;
+ // The user's indentifying feature, which maps to an instance of a view.
+ oneof identifying_feature {
+ common.v1.PhoneNumber phone_number = 3;
+ }
+}
+message GetUserResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The user doesn't exist
+ NOT_FOUND = 1;
+ // The user is no longer invited
+ NOT_INVITED = 2;
+ // The user exists, but at least one of their timelock accounts is unlocked
+ UNLOCKED_TIMELOCK_ACCOUNT = 3;
+ }
+ // The user associated with the identifier
+ User user = 2;
+ // The data container where the user can store a copy of their data
+ common.v1.DataContainerId data_container_id = 3;
+ // Field 4 is the deprecated kin_token_account_details
+ reserved 4;
+ // Metadata about the user based for the instance of their view
+ oneof metadata {
+ // Metadata that corresponds to a phone-based identifying feature.
+ PhoneMetadata phone = 5;
+ }
+ // Whether client internal flags are enabled for this user
+ bool enable_internal_flags = 6;
+ // Set of which airdrops the user is eligible to receive
+ repeated transaction.v2.AirdropType eligible_airdrops = 7;
+ // Wether the buy module is enabled for this user
+ bool enable_buy_module = 8;
+}
+message UpdatePreferencesRequest {
+ // The public key of the owner account that signed this request message.
+ common.v1.SolanaAccountId owner_account_id = 1;
+ // The data container for the copy of the contact list being added to.
+ common.v1.DataContainerId container_id = 2;
+ // The signature is of serialize(UpdatePreferencesRequest) without this field set
+ // using the private key of owner_account_id.
+ common.v1.Signature signature = 3;
+ // The user's locale, which is used for server-side localization of things like
+ // chat messages, pushes, etc. If no locale is set, or the provided locale isn't
+ // supported, then English is used as the default fallback.
+ //
+ // Note: This is required since it's the only preference. In the future, we'll add
+ // optional fields, where the subset of fields provided will be the ones that
+ // are updated.
+ common.v1.Locale locale = 4;
+}
+message UpdatePreferencesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The provided locale couldn't be parsed or recognized and is invalid.
+ INVALID_LOCALE = 1;
+ }
+}
+message LoginToThirdPartyAppRequest {
+ // The intent ID identifying the instance of the login flow.
+ common.v1.IntentId intent_id = 1;
+ // The relationship authority account logging in.
+ common.v1.SolanaAccountId user_id = 2;
+ // Signature of this message using the user private key, which authenticates
+ // the user.
+ common.v1.Signature signature = 3;
+}
+message LoginToThirdPartyAppResponse {
+ Result result = 1;
+ enum Result {
+ // This supports idempotency. The same login with the same user will result
+ // in OK.
+ OK = 0;
+ // There is no request for the provided intent ID.
+ REQUEST_NOT_FOUND = 1;
+ // The request requires a payment. Call SubmitIntent instead.
+ PAYMENT_REQUIRED = 2;
+ // The request exists, but doesn't support login.
+ LOGIN_NOT_SUPPORTED = 3;
+ // A login with a different user already exists
+ DIFFERENT_LOGIN_EXISTS = 4;
+ // The provided account is not valid for login. It must be a relationship
+ // account with the correct identifier specified in the original request.
+ INVALID_ACCOUNT = 5;
+ }
+}
+message GetLoginForThirdPartyAppRequest {
+ // The intent ID identifying the instance of the login flow.
+ common.v1.IntentId intent_id = 1;
+ // Owner account owned by the third party used in domain verification.
+ common.v1.SolanaAccountId verifier = 2;
+ // Signature of this message using the verifier private key, which in addition
+ // to domain verification, authenticates the third party.
+ common.v1.Signature signature = 3;
+}
+message GetLoginForThirdPartyAppResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // There is no request for the provided intent ID.
+ REQUEST_NOT_FOUND = 1;
+ // The request exists, but doesn't support login.
+ LOGIN_NOT_SUPPORTED = 2;
+ // The intent supports login, but it hasn't been submitted. There is no
+ // logged in user yet.
+ NO_USER_LOGGED_IN = 3;
+ }
+ // The relationship authority account that logged in.
+ common.v1.SolanaAccountId user_id = 2;
+}
+message GetTwitterUserRequest {
+ oneof query {
+ // The Twitter username to query against
+ string username = 1 ;
+ // The tip address to query against
+ common.v1.SolanaAccountId tip_address = 2;
+ }
+}
+message GetTwitterUserResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // The Twitter user doesn't exist or isn't linked with a Code account
+ NOT_FOUND = 1;
+ }
+ TwitterUser twitter_user = 2;
+}
+// User is the highest order of a form of identity within Code.
+//
+// Note: Users outside Code are modelled as relationship accounts
+message User {
+ // The user's ID
+ common.v1.UserId id = 1;
+ // The identifying features that are associated with the user
+ View view = 2;
+}
+// View is a well-defined set of identifying features. It is contrained to having
+// exactly one feature set at a time, for now.
+message View {
+ // The phone number associated with a user.
+ //
+ // Note: This field is mandatory as of right now, since it's the only one
+ // supported to date.
+ common.v1.PhoneNumber phone_number = 1;
+}
+message PhoneMetadata {
+ // State that determines whether a phone number is linked to the owner
+ // account. A phone number is linked if we can treat it as an alias.
+ // This is notably different from association, which answers the question
+ // of whether the number was linked at any point in time.
+ bool is_linked = 1;
+}
+message TwitterUser {
+ // Public key for a token account where tips are routed
+ common.v1.SolanaAccountId tip_address = 1;
+ // The user's username on Twitter
+ string username = 2 ;
+ // The user's friendly name on Twitter
+ string name = 3 ;
+ // URL to the user's Twitter profile picture
+ string profile_pic_url = 4 ;
+ // The type of Twitter verification associated with the user
+ VerifiedType verified_type = 5;
+ enum VerifiedType {
+ NONE = 0;
+ BLUE = 1;
+ BUSINESS = 2;
+ GOVERNMENT = 3;
+ }
+ // The number of followers the user has on Twitter
+ uint32 follower_count = 6;
+}
diff --git a/service/protos/.gitignore b/definitions/code/models/.gitignore
similarity index 100%
rename from service/protos/.gitignore
rename to definitions/code/models/.gitignore
diff --git a/service/models/build.gradle.kts b/definitions/code/models/build.gradle.kts
similarity index 94%
rename from service/models/build.gradle.kts
rename to definitions/code/models/build.gradle.kts
index 4d69ebcd9..b8c3842e4 100644
--- a/service/models/build.gradle.kts
+++ b/definitions/code/models/build.gradle.kts
@@ -12,7 +12,7 @@ version = "0.0.1"
group = "com.codeinc.gen"
dependencies {
- protobuf(project(":service:protos"))
+ protobuf(project(":definitions:code:protos"))
implementation(Libs.grpc_protobuf_lite)
implementation(Libs.grpc_stub)
@@ -28,7 +28,7 @@ kotlin {
}
android {
- namespace = "${Android.namespace}.service.models"
+ namespace = "${Android.codeNamespace}.service.models"
compileSdk = Android.compileSdkVersion
defaultConfig {
minSdk = Android.minSdkVersion
diff --git a/definitions/code/protos/.gitignore b/definitions/code/protos/.gitignore
new file mode 100644
index 000000000..9f2a07880
--- /dev/null
+++ b/definitions/code/protos/.gitignore
@@ -0,0 +1,2 @@
+build/
+.gradle/
diff --git a/definitions/code/protos/build.gradle.kts b/definitions/code/protos/build.gradle.kts
new file mode 100644
index 000000000..4cd4475f1
--- /dev/null
+++ b/definitions/code/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/service/protos/src/main/proto/account/v1/account_service.proto b/definitions/code/protos/src/main/proto/account/v1/account_service.proto
similarity index 100%
rename from service/protos/src/main/proto/account/v1/account_service.proto
rename to definitions/code/protos/src/main/proto/account/v1/account_service.proto
diff --git a/service/protos/src/main/proto/badge/v1/badge_service.proto b/definitions/code/protos/src/main/proto/badge/v1/badge_service.proto
similarity index 100%
rename from service/protos/src/main/proto/badge/v1/badge_service.proto
rename to definitions/code/protos/src/main/proto/badge/v1/badge_service.proto
diff --git a/service/protos/src/main/proto/chat/v1/chat_service.proto b/definitions/code/protos/src/main/proto/chat/v1/chat_service.proto
similarity index 100%
rename from service/protos/src/main/proto/chat/v1/chat_service.proto
rename to definitions/code/protos/src/main/proto/chat/v1/chat_service.proto
diff --git a/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto b/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto
new file mode 100644
index 000000000..9b5df3813
--- /dev/null
+++ b/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto
@@ -0,0 +1,420 @@
+syntax = "proto3";
+package code.chat.v2;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v2;chat";
+option java_package = "com.codeinc.gen.chat.v2";
+option objc_class_prefix = "CPBChatV2";
+import "common/v1/model.proto";
+import "transaction/v2/transaction_service.proto";
+import "google/protobuf/timestamp.proto";
+
+service Chat {
+ // GetChats gets the set of chats for an owner account using a paged API.
+ // This RPC is aware of all identities tied to the owner account.
+ rpc GetChats(GetChatsRequest) returns (GetChatsResponse);
+ // GetMessages gets the set of messages for a chat member using a paged API
+ rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse);
+ // StreamChatEvents streams chat events in real-time. Chat events include
+ // messages, pointer updates, etc.
+ //
+ // The streaming protocol is follows:
+ // 1. Client initiates a stream by sending an OpenChatEventStream message.
+ // 2. If an error is encoutered, a ChatStreamEventError message will be
+ // returned by server and the stream will be closed.
+ // 3. Server will immediately flush initial chat state.
+ // 4. New chat events will be pushed to the stream in real time as they
+ // are received.
+ //
+ // This RPC supports a keepalive protocol as follows:
+ // 1. Client initiates a stream by sending an OpenChatEventStream message.
+ // 2. Upon stream initialization, server begins the keepalive protocol.
+ // 3. Server sends a ping to the client.
+ // 4. Client responds with a pong as fast as possible, making note of
+ // the delay for when to expect the next ping.
+ // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated
+ // or is deemed to be unhealthy.
+ //
+ // Client notes:
+ // * Client should be careful to process events async, so any responses to pings are
+ // not delayed.
+ // * Clients should implement a reasonable backoff strategy upon continued timeout
+ // failures.
+ // * Clients that abuse pong messages may have their streams terminated by server.
+ //
+ // At any point in the stream, server will respond with events in real time as
+ // they are observed. Events sent over the stream should not affect the ping/pong
+ // protocol timings.
+ rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse);
+ // StartChat starts a chat. The RPC call is idempotent and will use existing
+ // chats whenever applicable within the context of message routing.
+ rpc StartChat(StartChatRequest) returns (StartChatResponse);
+ // SendMessage sends a message to a chat.
+ rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
+ // AdvancePointer advances a pointer in message history for a chat member.
+ rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse);
+ // SetMuteState configures a chat member's mute state.
+ rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse);
+ // NotifyIsTypingRequest notifies a chat that the sending member is typing.
+ //
+ // These requests are transient, and may be dropped at any point.
+ rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse);
+}
+message GetChatsRequest {
+ common.v1.SolanaAccountId owner = 1;
+ common.v1.Signature signature = 2;
+ uint32 page_size = 3;
+ Cursor cursor = 4;
+ Direction direction = 5;
+ enum Direction {
+ ASC = 0;
+ DESC = 1;
+ }
+}
+message GetChatsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ repeated Metadata chats = 2 ;
+}
+message GetMessagesRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.SolanaAccountId owner = 2;
+ common.v1.Signature signature = 3;
+ uint32 page_size = 4;
+ Cursor cursor = 5;
+ Direction direction = 6;
+ enum Direction {
+ ASC = 0;
+ DESC = 1;
+ }
+}
+message GetMessagesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+ repeated Message messages = 2 ;
+}
+message OpenChatEventStream {
+ common.v1.ChatId chat_id = 1;
+ common.v1.SolanaAccountId owner = 2;
+ common.v1.Signature signature = 3;
+}
+message ChatStreamEvent {
+ oneof type {
+ Message message = 1;
+ Pointer pointer = 2;
+ IsTyping is_typing = 3;
+ }
+}
+message ChatStreamEventBatch {
+ repeated ChatStreamEvent events = 2 ;
+}
+message ChatStreamEventError {
+ Code code = 1;
+ enum Code {
+ DENIED = 0;
+ }
+}
+message StreamChatEventsRequest {
+ oneof type {
+ OpenChatEventStream open_stream = 1;
+ common.v1.ClientPong pong = 2;
+ }
+}
+message StreamChatEventsResponse {
+ oneof type {
+ ChatStreamEventBatch events = 1;
+ common.v1.ServerPing ping = 2;
+ ChatStreamEventError error = 3;
+ }
+}
+message StartChatRequest {
+ common.v1.SolanaAccountId owner = 1;
+ common.v1.Signature signature = 2;
+ oneof parameters {
+ StartTwoWayChatParameters two_way_chat = 3;
+ }
+}
+// StartTwoWayChatParameters contains the parameters required to start
+// or recover a two way chat between the caller and the specified 'other_user'.
+//
+// The 'other_user' is currently the 'tip_address', normally retrieved from
+// user.Identity.GetTwitterUser(username).
+message StartTwoWayChatParameters {
+ // The account id of the user the caller wishes to chat with.
+ //
+ // This will be the `tip` (or equivalent) address.
+ common.v1.SolanaAccountId other_user = 1;
+ // The intent_id of the payment that initiated the chat/friendship.
+ //
+ // This field is optional. It is used as an optimization when the server has not
+ // yet observed the establishment of a friendship. In this case, the server will
+ // use the provided intent_id to verify the friendship.
+ //
+ // This is most likely to occur when initiating a chat with a user for the first
+ // time.
+ common.v1.IntentId intent_id = 2;
+}
+message StartChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // DENIED indicates the caller is not allowed to start/join the chat.
+ DENIED = 1;
+ // INVALID_PRAMETER indicates one of the parameters is invalid.
+ INVALID_PARAMETER = 2;
+ // PENDING indicates that the payment (for chat) intent is pending confirmation
+ // before the service will permit the creation of the chat. This can happen in
+ // cases where the block chain is particularly slow (beyond our RPC timeouts).
+ PENDING = 3;
+ // MISSING_IDENTITY indicates that there is no identity for the user (creator).
+ MISSING_IDENTITY = 4;
+ // USER_NOT_FOUND indicates that (one of) the target user's was not found.
+ USER_NOT_FOUND = 5;
+ }
+ // The chat to use if the RPC was successful.
+ Metadata chat = 2;
+}
+message SendMessageRequest {
+ common.v1.ChatId chat_id = 1;
+ // Allowed content types that can be sent by client:
+ // - TextContent
+ // - ThankYouContent
+ repeated Content content = 2 ;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message SendMessageResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ INVALID_CONTENT_TYPE = 2;
+ }
+ // The chat message that was sent if the RPC was succesful, which includes
+ // server-side metadata like the generated message ID and official timestamp
+ Message message = 2;
+}
+message AdvancePointerRequest {
+ common.v1.ChatId chat_id = 1;
+ Pointer pointer = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message AdvancePointerResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ MESSAGE_NOT_FOUND = 2;
+ }
+}
+message SetMuteStateRequest {
+ common.v1.ChatId chat_id = 1;
+ bool is_muted = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message SetMuteStateResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ CANT_MUTE = 2;
+ }
+}
+message NotifyIsTypingRequest {
+ common.v1.ChatId chat_id = 1;
+ bool is_typing = 2;
+ common.v1.SolanaAccountId owner = 3;
+ common.v1.Signature signature = 4;
+}
+message NotifyIsTypingResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message MessageId {
+ // A lexicographically sortable ID that can be used to sort source of
+ // chat history.
+ bytes value = 1 ;
+}
+message MemberId {
+ // The publically available 'deposit' address of the user.
+ bytes value = 1 ;
+}
+enum ChatType {
+ UNKNOWN_CHAT_TYPE = 0;
+ TWO_WAY = 1;
+ // GROUP = 3;
+}
+enum Platform {
+ UNKNOWN_PLATFORM = 0;
+ TWITTER = 1;
+}
+enum PointerType {
+ UNKNOWN_POINTER_TYPE = 0;
+ SENT = 1; // Always inferred by OK result in SendMessageResponse or message presence in a chat
+ DELIVERED = 2;
+ READ = 3;
+}
+// A chat
+//
+// todo: Support is_verified in a clean way
+message Metadata {
+ common.v1.ChatId chat_id = 1;
+ // The type of chat
+ ChatType type = 2 ;
+ // Cursor value for this chat for reference in subsequent GetChatsRequest
+ Cursor cursor = 3;
+ // The chat title, which is _only_ set by server if an explicit title
+ // was set. Otherwise, clients should fill in an appropriate chat title.
+ string title = 4 ;
+ // The members in this chat.
+ repeated Member members = 5 ;
+ // Whether or not the chat is muted (from the perspective of the caller).
+ bool is_muted = 6;
+ // Whether or not the chat is mutable (from the persective of the caller).
+ bool muteable = 7;
+ // Number of (estimated) unread message (from the perspective of the caller).
+ uint32 num_unread = 8;
+}
+// A message in a chat
+message Message {
+ // Globally unique ID for this message
+ MessageId message_id = 1;
+ // The chat member that sent the message. For NOTIFICATION chats, this field
+ // is omitted since the chat has exactly 1 member.
+ MemberId sender_id = 2;
+ // Ordered message content. A message may have more than one piece of content.
+ repeated Content content = 3 ;
+ // Timestamp this message was generated at. This value is also encoded in
+ // any time-based UUID message IDs.
+ google.protobuf.Timestamp ts = 4;
+ // Cursor value for this message for reference in a paged GetMessagesRequest
+ Cursor cursor = 5;
+}
+// A user in a chat
+message Member {
+ // Public AccountId (for is self...is derived via deposit address)
+ MemberId member_id = 1;
+ // The chat member's identity if it has been revealed.
+ //
+ // Multiple identities here? Well really only needs twitter/other handles
+ // Repeated PlatformHandles (where code doesn't matter)?
+ MemberIdentity identity = 2;
+ // Chat message state for this member.
+ //
+ // If set, the list may contain DELIVERED and READ pointers. SENT pointers
+ // are only shared between the sender and server, to indicate persistence.
+ //
+ // The server may wish to omit all pointers in various types of group chats
+ // or as relief valves.
+ repeated Pointer pointers = 3 ;
+ // If the member is the caller (where applicable), will be set to true.
+ bool is_self = 4;
+}
+// Identity to an external social platform that can be linked to a Code account
+message MemberIdentity {
+ // The external social platform linked to this chat member
+ Platform platform = 1 ;
+ // The chat member's username on the external social platform.
+ string username = 2 ;
+ // TODO: If we need the profile pic, we need display name...both can maybe be removed?
+ // If present, the display name of the user.
+ string display_name = 3 ;
+ // If present, the URL of the users profile pic.
+ string profile_pic_url = 4 ;
+}
+// Pointer in a chat indicating a user's message history state in a chat.
+message Pointer {
+ // The type of pointer indicates which user's message history state can be
+ // inferred from the pointer value. It is also possible to infer cross-pointer
+ // state. For example, if a chat member has a READ pointer for a message with
+ // ID N, then the DELIVERED pointer must be at least N.
+ PointerType type = 1 ;
+ // Everything at or before this message ID is considered to have the state
+ // inferred by the type of pointer.
+ MessageId value = 2;
+ // The chat member associated with this pointer state
+ MemberId member_id = 3;
+}
+// Content for a chat message
+message Content {
+ oneof type {
+ TextContent text = 1;
+ LocalizedContent localized = 2;
+ ExchangeDataContent exchange_data = 3;
+ NaclBoxEncryptedContent nacl_box = 4;
+ }
+}
+// Raw text content
+message TextContent {
+ string text = 1 ;
+}
+// Text content that is either a localization key that should be translated on
+// client, or a server-side translated piece of text.
+message LocalizedContent {
+ string key_or_text = 1 ;
+}
+// Exchange data content for movement of a value of Kin
+message ExchangeDataContent {
+ enum Verb {
+ UNKNOWN = 0;
+ GAVE = 1;
+ RECEIVED = 2;
+ WITHDREW = 3;
+ DEPOSITED = 4;
+ SENT = 5;
+ RETURNED = 6;
+ SPENT = 7;
+ PAID = 8;
+ PURCHASED = 9;
+ RECEIVED_TIP = 10;
+ SENT_TIP = 11;
+ }
+ // Verb describing how the amount of Kin was exchanged
+ //
+ // Note: The current definition is not suitable outside a NOTIFICATION chat
+ // as not enough context is provided as to which member this verb is
+ // associated with.
+ Verb verb = 1 ;
+ // An amount of Kin being exchanged
+ oneof exchange_data {
+ transaction.v2.ExchangeData exact = 2;
+ transaction.v2.ExchangeDataWithoutRate partial = 3;
+ }
+ // An ID that can be referenced to the source of the exchange of Kin
+ oneof reference {
+ common.v1.IntentId intent = 4;
+ common.v1.Signature signature = 5;
+ }
+ // TODO: We need to be able to provide the parties involved, such that
+ // such that client can render it correctly.
+}
+// Encrypted piece of content using NaCl box encryption
+message NaclBoxEncryptedContent {
+ // The sender's public key that is used to derive the shared private key for
+ // decryption for message content.
+ common.v1.SolanaAccountId peer_public_key = 1;
+ // Globally random nonce that is unique to this encrypted piece of content
+ bytes nonce = 2 ;
+ // The encrypted piece of message content
+ bytes encrypted_payload = 3 ;
+}
+// Opaque cursor used across paged APIs. Underlying bytes may change as paging
+// strategies evolve. Expected length value will vary based on the RPC being
+// executed.
+message Cursor {
+ bytes value = 1 ;
+}
+message IsTyping {
+ MemberId member_id = 1;
+ // is_typing indicates whether or not the user is typing.
+ // If false, the user has explicitly stopped typing.
+ bool is_typing = 2;
+}
diff --git a/service/protos/src/main/proto/common/v1/model.proto b/definitions/code/protos/src/main/proto/common/v1/model.proto
similarity index 100%
rename from service/protos/src/main/proto/common/v1/model.proto
rename to definitions/code/protos/src/main/proto/common/v1/model.proto
diff --git a/service/protos/src/main/proto/contact/v1/contact_list_service.proto b/definitions/code/protos/src/main/proto/contact/v1/contact_list_service.proto
similarity index 100%
rename from service/protos/src/main/proto/contact/v1/contact_list_service.proto
rename to definitions/code/protos/src/main/proto/contact/v1/contact_list_service.proto
diff --git a/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto b/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto
new file mode 100644
index 000000000..5458a224b
--- /dev/null
+++ b/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto
@@ -0,0 +1,29 @@
+syntax = "proto3";
+package code.currency.v1;
+option go_package = "github.com/code-payments/code-protobuf-api/generated/go/currency/v1;currency";
+option java_package = "com.codeinc.gen.currency.v1";
+option objc_class_prefix = "CPBCurrencyV1";
+
+import "google/protobuf/timestamp.proto";
+service Currency {
+ // GetAllRates returns the exchange rates for Kin against all available currencies
+ rpc GetAllRates(GetAllRatesRequest) returns (GetAllRatesResponse);
+}
+message GetAllRatesRequest {
+ // If timestamp is included, the returned rate will be the most recent available
+ // exchange rate prior to the provided timestamp within the same day. Otherwise,
+ // the latest rates will be returned.
+ google.protobuf.Timestamp timestamp = 1;
+}
+message GetAllRatesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // No currency data is available for the requested timestamp.
+ MISSING_DATA = 1;
+ }
+ // The time the exchange rates were observed
+ google.protobuf.Timestamp as_of = 2;
+ // The price of 1 Kin in different currencies, keyed on 3- or 4- letter lowercase currency code.
+ map rates = 3 ;
+}
diff --git a/service/protos/src/main/proto/device/v1/device_service.proto b/definitions/code/protos/src/main/proto/device/v1/device_service.proto
similarity index 100%
rename from service/protos/src/main/proto/device/v1/device_service.proto
rename to definitions/code/protos/src/main/proto/device/v1/device_service.proto
diff --git a/service/protos/src/main/proto/invite/v2/invite_service.proto b/definitions/code/protos/src/main/proto/invite/v2/invite_service.proto
similarity index 100%
rename from service/protos/src/main/proto/invite/v2/invite_service.proto
rename to definitions/code/protos/src/main/proto/invite/v2/invite_service.proto
diff --git a/service/protos/src/main/proto/messaging/v1/messaging_service.proto b/definitions/code/protos/src/main/proto/messaging/v1/messaging_service.proto
similarity index 100%
rename from service/protos/src/main/proto/messaging/v1/messaging_service.proto
rename to definitions/code/protos/src/main/proto/messaging/v1/messaging_service.proto
diff --git a/service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto b/definitions/code/protos/src/main/proto/micropayment/v1/micro_payment_service.proto
similarity index 100%
rename from service/protos/src/main/proto/micropayment/v1/micro_payment_service.proto
rename to definitions/code/protos/src/main/proto/micropayment/v1/micro_payment_service.proto
diff --git a/service/protos/src/main/proto/phone/v1/phone_verification_service.proto b/definitions/code/protos/src/main/proto/phone/v1/phone_verification_service.proto
similarity index 100%
rename from service/protos/src/main/proto/phone/v1/phone_verification_service.proto
rename to definitions/code/protos/src/main/proto/phone/v1/phone_verification_service.proto
diff --git a/service/protos/src/main/proto/push/v1/push_service.proto b/definitions/code/protos/src/main/proto/push/v1/push_service.proto
similarity index 100%
rename from service/protos/src/main/proto/push/v1/push_service.proto
rename to definitions/code/protos/src/main/proto/push/v1/push_service.proto
diff --git a/service/protos/src/main/proto/transaction/v2/transaction_service.proto b/definitions/code/protos/src/main/proto/transaction/v2/transaction_service.proto
similarity index 100%
rename from service/protos/src/main/proto/transaction/v2/transaction_service.proto
rename to definitions/code/protos/src/main/proto/transaction/v2/transaction_service.proto
diff --git a/service/protos/src/main/proto/user/v1/identity_service.proto b/definitions/code/protos/src/main/proto/user/v1/identity_service.proto
similarity index 100%
rename from service/protos/src/main/proto/user/v1/identity_service.proto
rename to definitions/code/protos/src/main/proto/user/v1/identity_service.proto
diff --git a/definitions/flipchat/models/.gitignore b/definitions/flipchat/models/.gitignore
new file mode 100644
index 000000000..9f2a07880
--- /dev/null
+++ b/definitions/flipchat/models/.gitignore
@@ -0,0 +1,2 @@
+build/
+.gradle/
diff --git a/definitions/flipchat/models/build.gradle.kts b/definitions/flipchat/models/build.gradle.kts
new file mode 100644
index 000000000..b2f74da5b
--- /dev/null
+++ b/definitions/flipchat/models/build.gradle.kts
@@ -0,0 +1,82 @@
+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.fc.gen"
+
+dependencies {
+ protobuf(project(":definitions:flipchat: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 {
+ namespace = "${Android.codeNamespace}.defs.fc.models"
+ compileSdk = Android.compileSdkVersion
+ defaultConfig {
+ minSdk = Android.minSdkVersion
+ targetSdk = Android.targetSdkVersion
+ buildToolsVersion = Android.buildToolsVersion
+ testInstrumentationRunner = Android.testInstrumentationRunner
+ }
+}
+
+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/definitions/flipchat/protos/.gitignore b/definitions/flipchat/protos/.gitignore
new file mode 100644
index 000000000..9f2a07880
--- /dev/null
+++ b/definitions/flipchat/protos/.gitignore
@@ -0,0 +1,2 @@
+build/
+.gradle/
diff --git a/definitions/flipchat/protos/build.gradle.kts b/definitions/flipchat/protos/build.gradle.kts
new file mode 100644
index 000000000..4cd4475f1
--- /dev/null
+++ b/definitions/flipchat/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/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto b/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto
new file mode 100644
index 000000000..96e76b04a
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto
@@ -0,0 +1,137 @@
+syntax = "proto3";
+package flipchat.account.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/account/v1;acountpb";
+option java_package = "com.codeinc.flipchat.gen.account.v1";
+option objc_class_prefix = "FCPBAccountV1";
+import "common/v1/common.proto";
+import "google/protobuf/timestamp.proto";
+
+service Account {
+ // Register registers a new user, bound to the provided PublicKey.
+ // If the PublicKey is already in use, the previous user account is returned.
+ rpc Register(RegisterRequest) returns (RegisterResponse);
+ // Login retrieves the UserId (and in the future, potentially other information)
+ // required for 'recovering' an account.
+ rpc Login(LoginRequest) returns (LoginResponse);
+ // AuthorizePublicKey authorizes an additional PublicKey to an account.
+ rpc AuthorizePublicKey(AuthorizePublicKeyRequest) returns (AuthorizePublicKeyResponse);
+ // RevokePublicKey revokes a public key from an account.
+ //
+ // There must be at least one public key per account. For now, any authorized public key
+ // may revoke another public key, but this may change in the future.
+ rpc RevokePublicKey(RevokePublicKeyRequest) returns (RevokePublicKeyResponse);
+ // GetPaymentDestination gets the payment destination for a UserId
+ rpc GetPaymentDestination(GetPaymentDestinationRequest) returns (GetPaymentDestinationResponse);
+ // GetUserFlags gets user-specific flags
+ rpc GetUserFlags(GetUserFlagsRequest) returns (GetUserFlagsResponse);
+}
+message RegisterRequest {
+ // PublicKey the public key that is authorized to perform actions on the
+ // registered users behalf.
+ common.v1.PublicKey public_key = 1;
+ // Signature of this message (without the signature), using the provided keypaid.
+ common.v1.Signature signature = 2;
+ // Deprecated: New account creation flow requires using profile service after IAP
+ string display_name = 3 ;
+}
+message RegisterResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ INVALID_SIGNATURE = 1;
+ INVALID_DISPLAY_NAME = 2;
+ DENIED = 3;
+ }
+ // Error reason contains the reason for the error, if the
+ // result > 1. This allows for server to impose moderation restrictions
+ // on user provided fields.
+ string error_reason = 2;
+ // The UserId associated with the account.
+ common.v1.UserId user_id = 3;
+}
+message LoginRequest {
+ // Timestamp is the timestamp the request was generated.
+ //
+ // The server may reject the request if the timestamp is too far off
+ // the current (server) time. This is to prevent replay attacks.
+ google.protobuf.Timestamp timestamp = 1;
+ common.v1.Auth auth = 2;
+}
+message LoginResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ INVALID_TIMESTAMP = 1;
+ DENIED = 2;
+ }
+ // UserId is the user associated with the PubKey/Auth.
+ common.v1.UserId user_id = 2;
+}
+message AuthorizePublicKeyRequest {
+ // UserId to bound the new public key to.
+ common.v1.UserId user_id = 1;
+ // PublicKey of the account to be added.
+ common.v1.PublicKey public_key = 2;
+ // Signature of this message, not including auth or signature, using the
+ // new public key.
+ common.v1.Signature signature = 3;
+ common.v1.Auth auth = 4;
+}
+message AuthorizePublicKeyResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message RevokePublicKeyRequest {
+ // UserId to remove the public key from.
+ common.v1.UserId user_id = 1;
+ // PublicKey to remove.
+ common.v1.PublicKey public_key = 2;
+ common.v1.Auth auth = 4;
+}
+message RevokePublicKeyResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ LAST_PUB_KEY = 2;
+ }
+}
+message GetPaymentDestinationRequest {
+ // UserId to get the payment destination from.
+ common.v1.UserId user_id = 1;
+}
+message GetPaymentDestinationResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ // Payment destination for the UserId.
+ common.v1.PublicKey payment_destination = 2;
+}
+message GetUserFlagsRequest {
+ // UserId to get user flags for.
+ common.v1.UserId user_id = 1;
+ common.v1.Auth auth = 2;
+}
+message GetUserFlagsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+ UserFlags user_flags = 2;
+}
+message UserFlags {
+ // Is this user associated with a Flipchat staff member?
+ bool is_staff = 1;
+ // The fee payment amount for starting a new group
+ common.v1.PaymentAmount start_group_fee = 2;
+ // The destination account where fees should be paid to
+ common.v1.PublicKey fee_destination = 3;
+ // Is this a fully registered account using IAP for account creation?
+ bool is_registered_account = 4;
+}
diff --git a/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto b/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto
new file mode 100644
index 000000000..3c4d45c8e
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+package flipchat.badge.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/badge/v1;badgepb";
+option java_package = "com.codeinc.flipchat.gen.badge.v1";
+option objc_class_prefix = "FPBBadgeV1";
+import "common/v1/common.proto";
+
+service Badge {
+ // ResetBadgeCount resets an owner account's app icon badge count back to zero
+ rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse);
+}
+message ResetBadgeCountRequest {
+ common.v1.UserId user_id = 1;
+ common.v1.Auth auth = 2;
+}
+message ResetBadgeCountResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
diff --git a/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto b/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto
new file mode 100644
index 000000000..1a45d6633
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto
@@ -0,0 +1,588 @@
+syntax = "proto3";
+package flipchat.chat.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/chat/v1;chatpb";
+option java_package = "com.codeinc.flipchat.gen.chat.v1";
+option objc_class_prefix = "FCPBChatV1";
+import "common/v1/common.proto";
+import "messaging/v1/model.proto";
+import "google/protobuf/timestamp.proto";
+
+service Chat {
+ // StreamChatEvents streams all chat events for the requesting user.
+ //
+ // Chat events will include any update to a chat, including:
+ // 1. Metadata changes.
+ // 2. Membership changes.
+ // 3. Latest messages.
+ //
+ // The server will optionally filter out some events depending on load
+ // and chat type. For example, Broadcast chats may not receive latest
+ // messages.
+ //
+ // Clients should use GetMessages to backfill in any historical messages
+ // for a chat. It should be sufficient to rely on ChatEvents for some types
+ // of chats, but using StreamMessages provides a guarentee of message events
+ // for all chats.
+ rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse);
+ // GetChats gets the set of chats for an owner account using a paged API.
+ // This RPC is aware of all identities tied to the owner account.
+ rpc GetChats(GetChatsRequest) returns (GetChatsResponse);
+ // GetChat returns the metadata for a specific chat.
+ rpc GetChat(GetChatRequest) returns (GetChatResponse);
+ // StartChat starts a chat. The RPC call is idempotent and will use existing
+ // chats whenever applicable within the context of message routing.
+ rpc StartChat(StartChatRequest) returns (StartChatResponse);
+ // JoinChat joins a given chat.
+ rpc JoinChat(JoinChatRequest) returns (JoinChatResponse);
+ // LeaveChat leaves a given chat.
+ rpc LeaveChat(LeaveChatRequest) returns (LeaveChatResponse);
+ // OpenChat opens a chat up for messaging across all members
+ rpc OpenChat(OpenChatRequest) returns (OpenChatResponse);
+ // CloseChat closes a chat up for messaging to just the chat owner
+ rpc CloseChat(CloseChatRequest) returns (CloseChatResponse);
+ // SetDisplayName sets a chat's display name. If the display name isn't allowed,
+ // then a set of alternate suggestions may be provided
+ rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse);
+ // SetCoverCharge sets a chat's cover charge
+ //
+ // Deprecated: Use SetMessagingFee instead
+ rpc SetCoverCharge(SetCoverChargeRequest) returns (SetCoverChargeResponse);
+ // SetMessagingFee sets a chat's messaging fee
+ rpc SetMessagingFee(SetMessagingFeeRequest) returns (SetMessagingFeeResponse);
+ // GetMemberUpdates gets member updates for a given chat
+ rpc GetMemberUpdates(GetMemberUpdatesRequest) returns (GetMemberUpdatesResponse);
+ // PromoteUser promotes a user to an elevated permission state
+ rpc PromoteUser(PromoteUserRequest) returns (PromoteUserResponse);
+ // DemoteUser demotes a user to a lower permission state
+ rpc DemoteUser(DemoteUserRequest) returns (DemoteUserResponse);
+ // RemoveUser removes a user from a chat
+ rpc RemoveUser(RemoveUserRequest) returns (RemoveUserResponse);
+ // MuteUser mutes a user in the chat and removes their ability to send messages
+ rpc MuteUser(MuteUserRequest) returns (MuteUserResponse);
+ // MuteChat mutes a chat and disables push notifications
+ rpc MuteChat(MuteChatRequest) returns (MuteChatResponse);
+ // UnmuteChat unmutes a chat and enables push notifications
+ rpc UnmuteChat(UnmuteChatRequest) returns (UnmuteChatResponse);
+ // ReportUser reports a user for a given message
+ //
+ // todo: might belong in a different service long-term
+ rpc ReportUser(ReportUserRequest) returns (ReportUserResponse);
+}
+message StreamChatEventsRequest {
+ oneof type {
+ Params params = 1;
+ common.v1.ClientPong pong = 2;
+ }
+ message Params {
+ common.v1.Auth auth = 1;
+ // ts contains the time for stream open.
+ //
+ // It is used primarily as a nonce for auth. Server may reject
+ // timestamps that are too far in the future or past.
+ google.protobuf.Timestamp ts = 2;
+ }
+}
+message StreamChatEventsResponse {
+ oneof type {
+ common.v1.ServerPing ping = 1;
+ StreamError error = 2;
+ EventBatch events = 3;
+ }
+ message StreamError {
+ Code code = 1;
+ enum Code {
+ DENIED = 0;
+ }
+ }
+ message EventBatch {
+ repeated ChatUpdate updates = 1 ;
+ }
+ // ChatUpdate contains a set of updates for a given chat id.
+ //
+ // Only the relevant fields will be set on update. On initial
+ // stream open, all fields will be set, however.
+ message ChatUpdate {
+ common.v1.ChatId chat_id = 1;
+ // Metadata contains the latest (full) chat metadata.
+ //
+ // Deprecated: Use metadata_updates instead. For backwards compatibility
+ // this will only contain full metadata refreshes.
+ Metadata metadata = 2;
+ // MetadataUpdate contains updates to a chat metadata
+ repeated MetadataUpdate metadata_updates = 7;
+ // MemberUpdate contains an update to the membership set.
+ //
+ // Deprecated: Use member_updates instead. For backwards compatibility
+ // this will only contain full member refreshes.
+ MemberUpdate member_update = 3;
+ // MemberUpdate contains updates to the membership set.
+ repeated MemberUpdate member_updates = 8;
+ // Message contains the last known message of the chat.
+ messaging.v1.Message last_message = 4;
+ // Relevant update to a chat member's pointer state, where 'relevant' means
+ // "relevant to UI updates". For example, when a user has read the latest
+ // message.
+ PointerUpdate pointer = 5;
+ message PointerUpdate {
+ common.v1.UserId member = 1;
+ messaging.v1.Pointer pointer = 2;
+ }
+ // IsTyping indicates whether or not someone is typing in the group.
+ messaging.v1.IsTyping is_typing = 6;
+ }
+}
+message GetChatsRequest {
+ common.v1.QueryOptions query_options = 1;
+ common.v1.Auth auth = 2;
+}
+message GetChatsResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+ repeated Metadata chats = 2 ;
+}
+message GetChatRequest {
+ oneof identifier {
+ common.v1.ChatId chat_id = 1;
+ uint64 room_number = 2;
+ }
+ bool exclude_members = 9;
+ // Auth is an optional field that authenticates the call, which
+ // can be used to fill out extra information in the Metadata.
+ common.v1.Auth auth = 10;
+}
+message GetChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ // Metadata is the chat metadata, if result == OK.
+ //
+ // The contents of the metadata may change whether or not the
+ // call was authenticated.
+ Metadata metadata = 2;
+ // Members contains the chat members, if result == OK and were requested.
+ repeated Member members = 3;
+}
+message StartChatRequest {
+ oneof parameters {
+ StartTwoWayChatParameters two_way_chat = 1;
+ StartGroupChatParameters group_chat = 2;
+ }
+ // StartTwoWayChatParameters contains the parameters required to start
+ // or recover a two way chat between the caller and the specified 'other_user'.
+ //
+ // The 'other_user' is currently the 'tip_address', normally retrieved from
+ // user.Identity.GetTwitterUser(username).
+ message StartTwoWayChatParameters {
+ // The account id of the user the caller wishes to chat with.
+ common.v1.UserId other_user_id = 1;
+ }
+ message StartGroupChatParameters {
+ // A set of users (not including self) to initially set in the group chat.
+ repeated common.v1.UserId users = 1 ;
+ // Reserved for display_name field
+ reserved 2;
+ // Optional payment for creating the group. It's up to server to decide
+ // if the user is allowed to create a group without payment.
+ common.v1.IntentId payment_intent = 3;
+ }
+ common.v1.Auth auth = 10;
+}
+message StartChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ // DENIED indicates the caller is not allowed to start/join the chat.
+ DENIED = 1;
+ // USER_NOT_FOUND indicates that (one of) the target user's was not found.
+ USER_NOT_FOUND = 2;
+ }
+ // The chat to use, if result == OK.
+ Metadata chat = 2;
+ // Members contains the chat members, if result == OK.
+ repeated Member members = 3;
+}
+message StartGroupChatPaymentMetadata {
+ // The user creating the group chat, who will be the initial owner
+ common.v1.UserId user_id = 1;
+}
+message JoinChatRequest {
+ oneof identifier {
+ common.v1.ChatId chat_id = 1;
+ uint64 room_id = 2;
+ }
+ // Does the user want to join without the ability to send messages in the chat?
+ // If so, then payment_intent is not required? Otherwise, it is.
+ bool without_send_permission = 8;
+ // The payment for joining a chat, which is required for sending messages in
+ // the chat.
+ //
+ // Note: The chat owner can always bypass payment.
+ common.v1.IntentId payment_intent = 9;
+ common.v1.Auth auth = 10;
+}
+message JoinChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+ // The chat metadata, if join was successful.
+ Metadata metadata = 2;
+ // The members of the chat, if join was successful.
+ repeated Member members = 3;
+}
+message JoinChatPaymentMetadata {
+ // The user joining the chat
+ common.v1.UserId user_id = 1;
+ // The chat that the user is joining
+ common.v1.ChatId chat_id = 2;
+}
+message LeaveChatRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.Auth auth = 2;
+}
+message LeaveChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
+message OpenChatRequest {
+ // The chat that is being opened
+ common.v1.ChatId chat_id = 1;
+ common.v1.Auth auth = 2;
+}
+message OpenChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message CloseChatRequest {
+ /// The chat that is being closed
+ common.v1.ChatId chat_id = 1;
+ common.v1.Auth auth = 2;
+}
+message CloseChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message SetDisplayNameRequest {
+ common.v1.ChatId chat_id = 1;
+ string display_name = 2 ;
+ common.v1.Auth auth = 3;
+}
+message SetDisplayNameResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ CANT_SET = 2;
+ }
+ repeated string alternate_suggestions = 2 ;
+}
+message SetCoverChargeRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.PaymentAmount cover_charge = 2;
+ common.v1.Auth auth = 3;
+}
+message SetCoverChargeResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ CANT_SET = 2;
+ }
+}
+message SetMessagingFeeRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.PaymentAmount messaging_fee = 2;
+ common.v1.Auth auth = 3;
+}
+message SetMessagingFeeResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ CANT_SET = 2;
+ }
+}
+message GetMemberUpdatesRequest {
+ common.v1.ChatId chat_id = 1;
+ // If not provided, a full refresh is performed. Server may also choose
+ // to compact updates into a full or individual refresh.
+ common.v1.PagingToken paging_token = 2;
+ // Auth is an optional field that authenticates the call, which
+ // can be used to fill out extra information.
+ common.v1.Auth auth = 3;
+}
+message GetMemberUpdatesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ repeated MemberUpdate updates = 2 ;
+}
+message PromoteUserRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.UserId user_id = 2;
+ // Enables send permissions when value is true
+ bool enable_send_permission = 3;
+ common.v1.Auth auth = 100;
+}
+message PromoteUserResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message DemoteUserRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.UserId user_id = 2;
+ // Disables send permissions when value is true
+ bool disable_send_permission = 3;
+ common.v1.Auth auth = 100;
+}
+message DemoteUserResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message RemoveUserRequest{
+ common.v1.ChatId chat_id = 1;
+ common.v1.UserId user_id = 2;
+ common.v1.Auth auth = 3;
+}
+message RemoveUserResponse{
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message MuteUserRequest{
+ common.v1.ChatId chat_id = 1;
+ common.v1.UserId user_id = 2;
+ common.v1.Auth auth = 3;
+}
+message MuteUserResponse{
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message MuteChatRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.Auth auth = 2;
+}
+message MuteChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message UnmuteChatRequest {
+ common.v1.ChatId chat_id = 1;
+ common.v1.Auth auth = 2;
+}
+message UnmuteChatResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
+message ReportUserRequest{
+ common.v1.UserId user_id = 1;
+ messaging.v1.MessageId message_id = 2;
+ common.v1.Auth auth = 3;
+}
+message ReportUserResponse{
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
+message Metadata {
+ common.v1.ChatId chat_id = 1;
+ // The type of chat
+ ChatType type = 2 ;
+ enum ChatType {
+ UNKNOWN = 0;
+ TWO_WAY = 1;
+ GROUP = 2;
+ }
+ // The chat display name
+ string display_name = 3 ;
+ // If non-zero, the room number associated with the chat.
+ uint64 room_number = 4;
+ // Are push notifications enabled for this chat (from the perspective of the caller)?
+ bool is_push_enabled = 5;
+ // Can the user disable push notifications for this chat using MuteChat?
+ bool can_disable_push = 6;
+ // Number of (estimated) unread message (from the perspective of the caller).
+ uint32 num_unread = 7;
+ // If there are more unread messages than indicated by num_unread. If this is
+ // true, client should show num_unread+ as the unread count.
+ bool has_more_unread = 11;
+ // Owner is the owner/creator of the chat.
+ //
+ // This is a super priviledge role, in which there can only be one.
+ // This role is displayed as a 'host' currently.
+ common.v1.UserId owner = 8;
+ // If present, the fee that must be paid to send a message as a non-regular
+ // chat member.
+ //
+ // This replaces the legacy cover charge mechanic, which is deprecated
+ common.v1.PaymentAmount messaging_fee = 9;
+ // The timestamp of the last activity in this chat
+ google.protobuf.Timestamp last_activity = 10;
+ // The status as to whether the room is open or closed. This may be
+ // omitted for chats where it doesn't apply. If not provided, it's
+ // safe to assume the chat is open indefinitely until otherwise provided.
+ OpenStatus open_status = 12;
+}
+// todo: In the future, we may add additional fields like open/closed until a timestamp, etc.
+// For backwards compatibility, client can always refer to is_currently_open for whether
+// a room is open right now or not for the purposes of sending messages.
+// todo: A better name for this
+message OpenStatus {
+ bool is_currently_open = 1;
+}
+message MetadataUpdate {
+ oneof kind {
+ FullRefresh full_refresh = 1;
+ UnreadCountChanged unread_count_changed = 2;
+ DisplayNameChanged display_name_changed = 3;
+ MessagingFeeChanged messaging_fee_changed = 4;
+ LastActivityChanged last_activity_changed = 5;
+ OpenStatusChanged open_status_changed = 6;
+ }
+ // Refreshes the entire chat metadata
+ message FullRefresh {
+ Metadata metadata = 1;
+ }
+ // New message in the chat has generated a new unread count
+ message UnreadCountChanged {
+ // Number of (estimated) unread message
+ uint32 num_unread = 1;
+ // If there are more unread messages than indicated by num_unread.
+ // If this is true, client should show num_unread+ as the unread count.
+ bool has_more_unread = 2;
+ }
+ // The chat display name has been updated to a new value
+ message DisplayNameChanged {
+ string new_display_name = 1 ;
+ }
+ // The chat messaging fee has been updated to a new value
+ message MessagingFeeChanged {
+ common.v1.PaymentAmount new_messaging_fee = 1;
+ }
+ // The last activity timestamp has changed to a newer value
+ message LastActivityChanged {
+ google.protobuf.Timestamp new_last_activity = 1;
+ }
+ // The open status has changed to a newer value
+ message OpenStatusChanged {
+ OpenStatus new_open_status = 1;
+ }
+}
+message Member {
+ common.v1.UserId user_id = 1;
+ // The chat member's identity/profile information.
+ //
+ // It is a light weight version of the users full profile, which
+ // can be retrieved from the Profile service.
+ MemberIdentity identity = 2;
+ // Chat message state for this member.
+ //
+ // If set, the list may contain DELIVERED and READ pointers. SENT pointers
+ // are only shared between the sender and server, to indicate persistence.
+ //
+ // The server may wish to omit all pointers in various types of group chats
+ // or as relief valves.
+ repeated messaging.v1.Pointer pointers = 3 ;
+ // If the member is the caller (where applicable), will be set to true.
+ bool is_self = 4;
+ // Does the chat member have permission to perform moderation actions in
+ // the chat?
+ bool has_moderator_permission = 5;
+ // Has the chat member been muted by a moderator? If so, they cannot send
+ // messages, even if they paid for the permission.
+ bool is_muted = 6;
+ // Does the chat member have permission to send messages in the chat? If
+ // not, the user is considered to be a spectator or listener. Otherwise,
+ // they are a speaker.
+ bool has_send_permission = 7;
+}
+message MemberIdentity {
+ // If present, the display name of the user.
+ string display_name = 1 ;
+ // If present, the URL of the users profile pic.
+ string profile_pic_url = 2 ;
+}
+message MemberUpdate {
+ oneof kind {
+ FullRefresh full_refresh = 1;
+ IndividualRefresh individual_refresh = 2;
+ Joined joined = 3;
+ Left left = 4;
+ Removed removed = 5;
+ Muted muted = 6;
+ Promoted promoted = 7;
+ Demoted demoted = 8;
+ }
+ common.v1.PagingToken paging_token = 1000;
+ // Refreshes the state of the entire chat membership
+ message FullRefresh {
+ repeated Member members = 1 ;
+ }
+ // Refreshes the state of an individual member in the chat
+ message IndividualRefresh {
+ Member member = 1;
+ }
+ // Member joined the chat via the JoinChat RPC
+ message Joined {
+ Member member = 1;
+ }
+ // Member left the chat via the LeaveChat RPC
+ message Left {
+ common.v1.UserId member = 1;
+ }
+ // Member was removed from the chat via the RemoveUser RPC
+ message Removed {
+ common.v1.UserId member = 1;
+ common.v1.UserId removed_by = 2;
+ }
+ // Member was muted in the chat via the MuteUser RPC
+ message Muted {
+ common.v1.UserId member = 1;
+ common.v1.UserId muted_by = 2;
+ }
+ // Member was promoted in the chat via the PromoteUser RPC
+ message Promoted {
+ common.v1.UserId member = 1;
+ common.v1.UserId promoted_by = 2;
+ bool send_permission_enabled = 3;
+ }
+ // Member was demoted in the chat via the DemoteUser RPC
+ message Demoted {
+ common.v1.UserId member = 1;
+ common.v1.UserId demoted_by = 2;
+ bool send_permission_disabled = 3;
+ }
+}
diff --git a/definitions/flipchat/protos/src/main/proto/common/v1/common.proto b/definitions/flipchat/protos/src/main/proto/common/v1/common.proto
new file mode 100644
index 000000000..618f3bddf
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/common/v1/common.proto
@@ -0,0 +1,95 @@
+syntax = "proto3";
+package flipchat.common.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/common/v1;commonpb";
+option java_package = "com.codeinc.flipchat.gen.common.v1";
+option objc_class_prefix = "FPBCommonV1";
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+
+// Auth provides an authentication information for RPCs/messages.
+//
+// Currently, only a single form is supported, but it may be useful in
+// the future to rely on session tokens instead.
+message Auth {
+ oneof kind {
+ // KeyPair uses pub key cryptography to verify.
+ KeyPair key_pair = 1;
+ }
+ // KeyPair uses a keypair to verify a message.
+ //
+ // The signature should be of the encapsulating proto message,
+ // _without_ the Auth section being set.
+ message KeyPair {
+ PublicKey pub_key = 1;
+ Signature signature = 2;
+ }
+}
+message UserId {
+ bytes value = 1 ;
+}
+message ChatId {
+ // Sufficient space is left for a consistent hash value, though other types
+ // of values may be used.
+ bytes value = 1 ;
+}
+// AppInstallId is a unque ID tied to a client app installation. It does not
+// identify a device. Value should remain private and not be shared across
+// installs.
+message AppInstallId {
+ string value = 1 ;
+}
+// Locale is a user locale consisting of a combination of language, script and region
+message Locale {
+ string value = 1;
+}
+message PublicKey {
+ bytes value = 1 ;
+}
+message IntentId {
+ bytes value = 1 ;
+}
+message Signature {
+ bytes value = 1 ;
+}
+message PaymentAmount {
+ uint64 quarks = 1;
+}
+message ServerPing {
+ // Timestamp the ping was sent on the stream, for client to get a sense
+ // of potential network latency
+ google.protobuf.Timestamp timestamp = 1;
+ // The delay server will apply before sending the next ping
+ google.protobuf.Duration ping_delay = 2;
+}
+message ClientPong {
+ // Timestamp the Pong was sent on the stream, for server to get a sense
+ // of potential network latency
+ google.protobuf.Timestamp timestamp = 1;
+}
+message PagingToken {
+ // Value contains a value of an identifier of the collection in common.
+ //
+ // For example, GetChats uses the ChatId.Value, where GetMessages uses MessageId.Value
+ // as the contents. It does _not_ contain the serialized ChatId or MessageId.
+ bytes value = 1 ;
+}
+message QueryOptions {
+ // PageSize limits the maximum page size of a response.
+ //
+ // Server may choose to return less items. If empty, server
+ // may select an arbitrary page size.
+ int64 page_size = 1;
+ // PagingToken is token that can be extracted from the identifier of a collection.
+ PagingToken paging_token = 2;
+ // Order is the order of elements, if applicable.
+ Order order = 3;
+ enum Order {
+ ASC = 0;
+ DESC = 1;
+ }
+}
+enum Platform {
+ UNKNOWN = 0;
+ APPLE = 1;
+ GOOGLE = 2;
+}
diff --git a/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto b/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto
new file mode 100644
index 000000000..f8fd43423
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto
@@ -0,0 +1,27 @@
+syntax = "proto3";
+package flipchat.iap.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/iap/v1;iappb";
+option java_package = "com.codeinc.flipchat.gen.iap.v1";
+option objc_class_prefix = "FPBIapV1";
+import "common/v1/common.proto";
+
+service Iap {
+ // OnPurchaseCompleted is called when an IAP has been completed
+ rpc OnPurchaseCompleted(OnPurchaseCompletedRequest) returns (OnPurchaseCompletedResponse);
+}
+message OnPurchaseCompletedRequest {
+ common.v1.Platform platform = 1;
+ Receipt receipt = 2;
+ common.v1.Auth auth = 3;
+}
+message OnPurchaseCompletedResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ INVALID_RECEIPT = 2; // Returned if the receipt is invalid, or not in a completed payment state
+ }
+}
+message Receipt {
+ string value = 1 ;
+}
diff --git a/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto b/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto
new file mode 100644
index 000000000..f2bcf4967
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto
@@ -0,0 +1,146 @@
+syntax = "proto3";
+package flipchat.messaging.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/messaging/v1;messagingpb";
+option java_package = "com.codeinc.flipchat.gen.messaging.v1";
+option objc_class_prefix = "FCPBMessagingV1";
+import "common/v1/common.proto";
+import "messaging/v1/model.proto";
+
+service Messaging {
+ // StreamMessages streams all messages/message states for the requested chat.
+ rpc StreamMessages(stream StreamMessagesRequest) returns (stream StreamMessagesResponse);
+ // GetMessage gets a single message in a chat
+ rpc GetMessage(GetMessageRequest) returns (GetMessageResponse);
+ // GetMessages gets the set of messages for a chat using a paged and batched APIs
+ rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse);
+ // SendMessage sends a message to a chat.
+ rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);
+ // AdvancePointer advances a pointer in message history for a chat member.
+ rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse);
+ // NotifyIsTypingRequest notifies a chat that the sending member is typing.
+ //
+ // These requests are transient, and may be dropped at any point.
+ rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse);
+}
+message StreamMessagesRequest {
+ oneof type {
+ Params params = 1;
+ common.v1.ClientPong pong = 2;
+ }
+ message Params {
+ common.v1.ChatId chat_id = 1;
+ // Deprecated: stream flushes are no longer supported
+ oneof resume {
+ MessageId last_known_message_id = 2;
+ bool latest_only = 3;
+ }
+ common.v1.Auth auth = 4;
+ }
+}
+message StreamMessagesResponse {
+ oneof type {
+ common.v1.ServerPing ping = 1;
+ StreamError error = 2;
+ MessageBatch messages = 3;
+ }
+ message StreamError {
+ Code code = 1;
+ enum Code {
+ DENIED = 0;
+ }
+ }
+}
+message GetMessageRequest {
+ common.v1.ChatId chat_id = 1;
+ MessageId message_id = 2;
+ common.v1.Auth auth = 3;
+}
+message GetMessageResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ NOT_FOUND = 2;
+ }
+ Message message = 2;
+}
+message GetMessagesRequest {
+ common.v1.ChatId chat_id = 1;
+ // If not set, defaults to an ascending query option without a page token and server-defined limit
+ oneof query {
+ common.v1.QueryOptions options = 2;
+ MessageIdBatch message_ids = 3;
+ }
+ common.v1.Auth auth = 5;
+}
+message GetMessagesResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+ repeated Message messages = 2 ;
+}
+message SendMessageRequest {
+ common.v1.ChatId chat_id = 1;
+ // Allowed content types that can be sent by client:
+ // - TextContent
+ // - ReactionContent
+ // - ReplyContent
+ // - TipContent
+ // - DeleteMessageContent
+ // - ReviewContent
+ repeated Content content = 2 ;
+ common.v1.Auth auth = 3;
+ // Intent ID for message contents that require a payment
+ common.v1.IntentId payment_intent = 4;
+}
+message SendMessageResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+ // The chat message that was sent if the RPC was succesful, which includes
+ // server-side metadata like the generated message ID and official timestamp
+ Message message = 2;
+}
+message SendMessageAsListenerPaymentMetadata {
+ // The chat where the message is being sent
+ common.v1.ChatId chat_id = 1;
+ // The user sending the message
+ common.v1.UserId user_id = 2;
+}
+message SendTipMessagePaymentMetadata {
+ // The chat where the message is being tipped
+ common.v1.ChatId chat_id = 1;
+ // The message that is being tipped
+ MessageId message_id = 2;
+ // The user sending the tip
+ common.v1.UserId tipper_id = 3;
+}
+message AdvancePointerRequest {
+ common.v1.ChatId chat_id = 1;
+ Pointer pointer = 2;
+ common.v1.Auth auth = 3;
+}
+message AdvancePointerResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ MESSAGE_NOT_FOUND = 2;
+ }
+}
+message NotifyIsTypingRequest {
+ common.v1.ChatId chat_id = 1;
+ bool is_typing = 2;
+ common.v1.Auth auth = 3;
+}
+message NotifyIsTypingResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ DENIED = 1;
+ }
+}
diff --git a/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto b/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto
new file mode 100644
index 000000000..be372f342
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto
@@ -0,0 +1,135 @@
+syntax = "proto3";
+package flipchat.messaging.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/messaging/v1;messagingpb";
+option java_package = "com.codeinc.flipchat.gen.messaging.v1";
+option objc_class_prefix = "FCPBMessagingV1";
+import "common/v1/common.proto";
+import "google/protobuf/timestamp.proto";
+
+message MessageId {
+ // A lexicographically sortable ID that can be used to sort source of
+ // chat history.
+ bytes value = 1 ;
+}
+message MessageIdBatch {
+ repeated MessageId message_ids = 1 ;
+}
+// A message in a chat
+message Message {
+ // Globally unique ID for this message
+ MessageId message_id = 1;
+ // The chat member that sent the message. For NOTIFICATION chats, this field
+ // is omitted since the chat has exactly 1 member.
+ common.v1.UserId sender_id = 2;
+ // Message content, which is currently guaranteed to have exactly one item.
+ repeated Content content = 3 ;
+ // Timestamp this message was generated at. This value is also encoded in
+ // any time-based UUID message IDs.
+ google.protobuf.Timestamp ts = 4;
+ // If sender_id is provided, were they off stage at the time of sending
+ // this message
+ bool was_sender_off_stage = 5;
+}
+message MessageBatch {
+ repeated Message messages = 1 ;
+}
+// Pointer in a chat indicating a user's message history state in a chat.
+message Pointer {
+ // The type of pointer indicates which user's message history state can be
+ // inferred from the pointer value. It is also possible to infer cross-pointer
+ // state. For example, if a chat member has a READ pointer for a message with
+ // ID N, then the DELIVERED pointer must be at least N.
+ Type type = 1 ;
+ enum Type {
+ UNKNOWN = 0;
+ SENT = 1; // Always inferred by OK result in SendMessageResponse or message presence in a chat
+ DELIVERED = 2;
+ READ = 3;
+ }
+ // Everything at or before this message ID is considered to have the state
+ // inferred by the type of pointer.
+ MessageId value = 2;
+}
+message IsTyping {
+ common.v1.UserId user_id = 1;
+ // is_typing indicates whether or not the user is typing.
+ // If false, the user has explicitly stopped typing.
+ bool is_typing = 2;
+}
+// Content for a chat message
+message Content {
+ oneof type {
+ TextContent text = 1;
+ LocalizedAnnouncementContent localized_announcement = 2;
+ ReactionContent reaction = 5;
+ ReplyContent reply = 6;
+ TipContent tip = 7;
+ DeleteMessageContent deleted = 8;
+ ReviewContent review = 9;
+ ActionableAnnouncementContent actionable_announcement = 10;
+ }
+ reserved 3; // ExchangeDataContent
+ reserved 4; // NaclBoxEncryptedContent
+}
+// Raw text content
+message TextContent {
+ string text = 1 ;
+}
+// LocalizedAnnouncementContent content is an annoucement that is either a
+// localization key that should be translated on client, or a server-side
+// translated piece of text.
+message LocalizedAnnouncementContent {
+ string key_or_text = 1 ;
+ // todo: define arguments list
+ reserved 2;
+}
+// ActionableAnnouncementContent is like LocalizedAnnouncementContent, but
+// contains additional metadata for actions
+message ActionableAnnouncementContent {
+ string key_or_text = 1 ;
+ // todo: define arguments list
+ reserved 2;
+ // An action that can be taken by a user
+ Action action = 3;
+ message Action {
+ oneof type {
+
+ ShareRoomLink share_room_link = 1;
+ }
+ // Displays a button to share a link to a room
+ message ShareRoomLink {
+ }
+ }
+}
+// Emoji reaction to another message
+message ReactionContent {
+ // The message ID of the message this reaction is associated with
+ MessageId original_message_id = 1 ;
+ // The emoji or reaction symbol
+ string emoji = 2 ;
+}
+// Text reply of another message
+message ReplyContent {
+ // The message ID of the message this reply is referencing
+ MessageId original_message_id = 1 ;
+ // The reply text, which can be handled similarly to TextContent
+ string reply_text = 2 ;
+}
+message TipContent {
+ // The message ID of the message this tip is referencing
+ MessageId original_message_id = 1 ;
+ // The amount tipped for the message
+ common.v1.PaymentAmount tip_amount = 2;
+}
+message DeleteMessageContent {
+ // The message ID of the message that was deleted
+ MessageId original_message_id = 1 ;
+}
+message ReviewContent {
+ // The message ID of the message that is being reviewed. Currently, only
+ // off stage messages can be reviewed
+ MessageId original_message_id = 1 ;
+ // Whether the message has been approved. In the event of multiple reviews,
+ // the first message in the message log takes priority.
+ bool is_approved = 2;
+}
diff --git a/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto b/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto
new file mode 100644
index 000000000..2cbccf7df
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto
@@ -0,0 +1,10 @@
+syntax = "proto3";
+package flipchat.profile.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/profile/v1;profilepb";
+option java_package = "com.codeinc.flipchat.gen.profile.v1";
+option objc_class_prefix = "FCPBProfileV1";
+
+message UserProfile {
+ // DisplayName is the display name of the user (if found).
+ string display_name = 1 ;
+}
diff --git a/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto b/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto
new file mode 100644
index 000000000..9ceac1379
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto
@@ -0,0 +1,40 @@
+syntax = "proto3";
+package flipchat.profile.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/profile/v1;profilepb";
+option java_package = "com.codeinc.flipchat.gen.profile.v1";
+option objc_class_prefix = "FCPBProfileV1";
+import "common/v1/common.proto";
+import "profile/v1/model.proto";
+
+service Profile {
+ rpc GetProfile(GetProfileRequest) returns (GetProfileResponse);
+ rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse);
+}
+message GetProfileRequest {
+ common.v1.UserId user_id = 1;
+}
+message GetProfileResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ NOT_FOUND = 1;
+ }
+ // UserProfile, if found.
+ //
+ // Some fields may or may not be set, depending on the scope of request
+ // in the future.
+ UserProfile user_profile = 2;
+}
+message SetDisplayNameRequest {
+ // DisplayName is the new name to set.
+ string display_name = 1 ;
+ common.v1.Auth auth = 10;
+}
+message SetDisplayNameResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ INVALID_DISPLAY_NAME = 1;
+ DENIED = 2;
+ }
+}
diff --git a/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto b/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto
new file mode 100644
index 000000000..76e983f6d
--- /dev/null
+++ b/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto
@@ -0,0 +1,58 @@
+syntax = "proto3";
+package flipchat.push.v1;
+option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/push/v1;pushpb";
+option java_package = "com.codeinc.flipchat.gen.push.v1";
+option objc_class_prefix = "FPBPushV1";
+import "common/v1/common.proto";
+
+service Push {
+ // AddToken adds a push token associated with a user.
+ rpc AddToken(AddTokenRequest) returns (AddTokenResponse);
+ // DeleteToken removes a specific push token from a user.
+ //
+ // Deprecated: Use DeleteTokens intead
+ rpc DeleteToken(DeleteTokenRequest) returns (DeleteTokenResponse);
+ // DeleteTokens removes all push tokens within an app install for a user
+ rpc DeleteTokens(DeleteTokensRequest) returns (DeleteTokensResponse);
+}
+enum TokenType {
+ UNKNOWN = 0;
+ // FCM registration token for an Android device
+ FCM_ANDROID = 1;
+ // FCM registration token or an iOS device
+ FCM_APNS = 2;
+}
+message AddTokenRequest {
+ TokenType token_type = 1;
+ string push_token = 2 ;
+ common.v1.AppInstallId app_install = 3;
+ common.v1.Auth auth = 4;
+}
+message AddTokenResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ INVALID_PUSH_TOKEN = 1;
+ }
+}
+message DeleteTokenRequest {
+ TokenType token_type = 1;
+ string push_token = 2 ;
+ common.v1.Auth auth = 3;
+}
+message DeleteTokenResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
+message DeleteTokensRequest {
+ common.v1.AppInstallId app_install = 1;
+ common.v1.Auth auth = 2;
+}
+message DeleteTokensResponse {
+ Result result = 1;
+ enum Result {
+ OK = 0;
+ }
+}
diff --git a/fastlane/Appfile b/fastlane/Appfile
index 0cc86ca8a..286a09891 100644
--- a/fastlane/Appfile
+++ b/fastlane/Appfile
@@ -1,2 +1 @@
json_key_file(ENV["SERVICE_ACCOUNT_KEY_JSON"]) # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
-package_name("com.getcode") # e.g. com.krausefx.app
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index a7aa198f8..423288761 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -16,46 +16,53 @@
default_platform(:android)
platform :android do
- desc "Runs all the tests"
- lane :test do
- gradle(task: "test")
+ desc "Runs all the tests for Code"
+ lane :code_tests do
+ gradle(task: "app:testDebug")
end
- desc "Build and Deploy a new alpha version to the Google Play"
- lane :deploy_alpha do
- #puts "Patch version for this build will be " + ENV["BUILD_NUMBER"]
- gradle(
- task: "clean bundle", #"clean app:bundleRelease",
- build_type: "release",
- properties: {
- #"versionPatch" => ENV["BUILD_NUMBER"],
- "android.injected.signing.store.file" => "key/key",
- "android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
- "android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
- "android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
- }
- )
+ desc "Runs all the tests for Flipchat"
+ lane :fc_tests do
+ gradle(task: "flipchatApp:testDebug")
+ end
- validate_play_store_json_key(
- json_key: ENV["SERVICE_ACCOUNT_KEY_JSON"]
- )
+ desc "Build and Deploy a new internal version of Code to the Google Play"
+ lane :deploy_code_internal do
+ #puts "Patch version for this build will be " + ENV["BUILD_NUMBER"]
+ gradle(
+ task: "clean app:bundle", #"clean app:bundleRelease",
+ build_type: "release",
+ properties: {
+ #"versionPatch" => ENV["BUILD_NUMBER"],
+ "android.injected.signing.store.file" => "key/key",
+ "android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
+ "android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
+ "android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
+ }
+ )
- upload_to_play_store(
- track: "alpha",
- aab: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
- skip_upload_apk: true,
- skip_upload_changelogs: true,
- skip_upload_images: true,
- mapping: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]
- # mapping: mapping_file_exists() ? Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] : nil
- )
- end
+ validate_play_store_json_key(
+ json_key: ENV["SERVICE_ACCOUNT_KEY_JSON"]
+ )
- desc "Build and Deploy a new internal version to the Google Play"
- lane :deploy_internal do
+ upload_to_play_store(
+ track: "internal",
+ aab: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
+ skip_upload_apk: true,
+ skip_upload_changelogs: true,
+ skip_upload_images: true,
+ package_name: "com.getcode",
+ mapping: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]
+ # mapping: mapping_file_exists() ? Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] : nil
+ )
+ end
+
+
+ desc "Build and Deploy a new internal version of Flipchat to the Google Play Store"
+ lane :deploy_fc_internal do
#puts "Patch version for this build will be " + ENV["BUILD_NUMBER"]
gradle(
- task: "clean bundle", #"clean app:bundleRelease",
+ task: "clean flipchatApp:bundle", #"clean app:bundleRelease",
build_type: "release",
properties: {
#"versionPatch" => ENV["BUILD_NUMBER"],
@@ -66,6 +73,17 @@ platform :android do
}
)
+ bundletool(
+ ks_path: "key/key",
+ ks_password: ENV["STORE_PASSWORD"],
+ ks_key_alias: ENV["KEY_ALIAS"],
+ ks_key_alias_password: ENV["KEY_PASSWORD"],
+ bundletool_version: '1.10.0',
+ aab_path: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
+ apk_output_path: "flipchatApp/build/outputs/apk/release",
+ verbose: true,
+ )
+
validate_play_store_json_key(
json_key: ENV["SERVICE_ACCOUNT_KEY_JSON"]
)
@@ -76,6 +94,7 @@ platform :android do
skip_upload_apk: true,
skip_upload_changelogs: true,
skip_upload_images: true,
+ package_name: "xyz.flipchat.app",
mapping: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH]
# mapping: mapping_file_exists() ? Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] : nil
)
diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile
new file mode 100644
index 000000000..860473d88
--- /dev/null
+++ b/fastlane/Pluginfile
@@ -0,0 +1,5 @@
+# Autogenerated by fastlane
+#
+# Ensure this file is checked in to source control!
+
+gem 'fastlane-plugin-bundletool'
diff --git a/fastlane/README.md b/fastlane/README.md
new file mode 100644
index 000000000..67ce7eba9
--- /dev/null
+++ b/fastlane/README.md
@@ -0,0 +1,48 @@
+fastlane documentation
+----
+
+# Installation
+
+Make sure you have the latest version of the Xcode command line tools installed:
+
+```sh
+xcode-select --install
+```
+
+For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
+
+# Available Actions
+
+## Android
+
+### android test
+
+```sh
+[bundle exec] fastlane android test
+```
+
+Runs all the tests
+
+### android deploy_code_internal
+
+```sh
+[bundle exec] fastlane android deploy_code_internal
+```
+
+Build and Deploy a new internal version of Code to the Google Play
+
+### android deploy_fc_internal
+
+```sh
+[bundle exec] fastlane android deploy_fc_internal
+```
+
+Build and Deploy a new internal version of Flipchat to the Google Play Store
+
+----
+
+This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
+
+More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
+
+The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
diff --git a/fastlane/report.xml b/fastlane/report.xml
new file mode 100644
index 000000000..19904ccea
--- /dev/null
+++ b/fastlane/report.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/flipchatApp/.gitignore b/flipchatApp/.gitignore
new file mode 100644
index 000000000..80efa742e
--- /dev/null
+++ b/flipchatApp/.gitignore
@@ -0,0 +1,3 @@
+build/
+.gradle/
+google-services.json
diff --git a/flipchatApp/build.gradle.kts b/flipchatApp/build.gradle.kts
new file mode 100644
index 000000000..522444e0c
--- /dev/null
+++ b/flipchatApp/build.gradle.kts
@@ -0,0 +1,247 @@
+import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
+import org.jetbrains.kotlin.cli.common.toBooleanLenient
+
+plugins {
+ id(Plugins.android_application)
+ id(Plugins.kotlin_android)
+ id(Plugins.kotlin_parcelize)
+ id(Plugins.kotlin_kapt)
+ id(Plugins.kotlin_serialization)
+ id(Plugins.androidx_navigation_safeargs)
+ id(Plugins.hilt)
+ id(Plugins.google_services)
+ id(Plugins.firebase_crashlytics)
+ id(Plugins.firebase_perf)
+ id(Plugins.bugsnag)
+ id(Plugins.secrets_gradle_plugin)
+ id(Plugins.versioning_gradle_plugin)
+}
+
+val contributorsSigningConfig = ContributorsSignatory(rootProject)
+val appNamespace = "${Android.flipchatNamespace}.app"
+
+android {
+ // static namespace
+ namespace = appNamespace
+ compileSdk = Android.compileSdkVersion
+
+ defaultConfig {
+ versionCode = versioning.getVersionCode()
+ versionName = Packaging.Flipchat.versionName
+ applicationId = appNamespace
+ minSdk = Android.minSdkVersion
+ targetSdk = Android.targetSdkVersion
+ buildToolsVersion = Android.buildToolsVersion
+ testInstrumentationRunner = Android.testInstrumentationRunner
+
+ buildConfigField("String", "MIXPANEL_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "MIXPANEL_API_KEY")}\"")
+ buildConfigField("String", "KADO_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "KADO_API_KEY")}\"")
+ buildConfigField("Boolean", "NOTIFY_ERRORS", "false")
+ }
+
+ signingConfigs {
+ create("contributors") {
+ storeFile = contributorsSigningConfig.keystore
+ storePassword = contributorsSigningConfig.keystorePassword
+ keyAlias = contributorsSigningConfig.keyAlias
+ keyPassword = contributorsSigningConfig.keyPassword
+ }
+ }
+
+ buildFeatures {
+ buildConfig = true
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Versions.compose_compiler
+ }
+
+ buildTypes {
+ getByName("release") {
+ resValue("string", "applicationId", appNamespace)
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ getByName("debug") {
+ applicationIdSuffix = ".dev"
+ resValue("string", "applicationId", "${appNamespace}.dev")
+ signingConfig = signingConfigs.getByName("contributors")
+
+ val debugMinifyEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_MINIFY", "false").toBooleanLenient() ?: false
+ isMinifyEnabled = debugMinifyEnabled
+ isShrinkResources = debugMinifyEnabled
+
+ if (debugMinifyEnabled) {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+
+ configure {
+ mappingFileUploadEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_CRASHLYTICS_UPLOAD", "false").toBooleanLenient() ?: false
+ }
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility(Versions.java)
+ targetCompatibility(Versions.java)
+ isCoreLibraryDesugaringEnabled = true
+ }
+
+ java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(Versions.java))
+ }
+ }
+
+ kotlinOptions {
+ jvmTarget = Versions.java
+ freeCompilerArgs += listOf(
+ "-opt-in=kotlin.RequiresOptIn"
+ )
+ }
+ packaging {
+ resources.excludes.add("**/*.proto")
+ resources.excludes.add("META-INF/LICENSE.md")
+ resources.excludes.add("META-INF/LICENSE-notice.md")
+ }
+}
+
+dependencies {
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
+ implementation(project(":services:flipchat:sdk"))
+
+ implementation(project(":libs:datetime"))
+ implementation(project(":libs:locale"))
+ implementation(project(":libs:vibrator"))
+ implementation(project(":libs:encryption:ed25519"))
+ implementation(project(":libs:encryption:keys"))
+ implementation(project(":libs:encryption:mnemonic"))
+ implementation(project(":libs:encryption:utils"))
+ implementation(project(":libs:crypto:kin"))
+ implementation(project(":libs:currency"))
+ implementation(project(":libs:logging"))
+ implementation(project(":libs:messaging"))
+ implementation(project(":libs:network:exchange"))
+ implementation(project(":libs:network:connectivity"))
+ implementation(project(":libs:opengraph"))
+ implementation(project(":libs:permissions"))
+ implementation(project(":libs:quickresponse"))
+ implementation(project(":libs:requests"))
+ implementation(project(":ui:components"))
+ implementation(project(":ui:navigation"))
+ implementation(project(":ui:resources"))
+ implementation(project(":ui:theme"))
+ implementation(project(":vendor:tipkit:tipkit-m2"))
+
+ coreLibraryDesugaring(Libs.android_desugaring)
+
+ //standard libraries
+ implementation(Libs.kotlinx_collections_immutable)
+ implementation(Libs.kotlinx_serialization_json)
+ implementation(Libs.kotlinx_datetime)
+ implementation(Libs.androidx_core)
+ implementation(Libs.androidx_constraint_layout)
+ implementation(Libs.androidx_lifecycle_runtime)
+ implementation(Libs.androidx_lifecycle_viewmodel)
+ implementation(Libs.androidx_navigation_fragment)
+ implementation(Libs.androidx_navigation_ui)
+
+ //hilt dependency injection
+ implementation(Libs.hilt)
+ implementation("androidx.webkit:webkit:1.12.1")
+ kapt(Libs.hilt_android_compiler)
+ kapt(Libs.hilt_compiler)
+ androidTestImplementation(Libs.hilt)
+ androidTestImplementation(Libs.hilt_android_test)
+ kaptAndroidTest(Libs.hilt_android_compiler)
+ testImplementation(Libs.hilt_android_test)
+ kaptTest(Libs.hilt_android_compiler)
+
+ androidTestImplementation("io.mockk:mockk:1.13.12")
+
+ //Jetpack compose
+ implementation(platform(Libs.compose_bom))
+ implementation(Libs.compose_ui)
+ debugImplementation(Libs.compose_ui_tools)
+ implementation(Libs.compose_accompanist)
+ implementation(Libs.compose_foundation)
+ implementation(Libs.compose_material)
+ implementation(Libs.compose_materialIconsExtended)
+ implementation(Libs.compose_activities)
+ implementation(Libs.compose_view_models)
+ implementation(Libs.compose_livedata)
+ implementation(Libs.compose_navigation)
+ implementation(Libs.compose_paging)
+ implementation(Libs.compose_webview)
+
+ implementation(Libs.androidx_biometrics)
+
+ implementation(Libs.androidx_activity)
+
+ // cameraX
+ implementation(Libs.androidx_camerax_core)
+ implementation(Libs.androidx_camerax_camera2)
+ implementation(Libs.androidx_camerax_lifecycle)
+ implementation(Libs.androidx_camerax_view)
+
+ implementation(Libs.coil3)
+ implementation(Libs.coil3_network)
+
+ implementation(Libs.androidx_browser)
+ implementation(Libs.androidx_constraint_layout_compose)
+
+ implementation(Libs.rxjava)
+ implementation(Libs.rxandroid)
+
+ implementation(Libs.slf4j)
+ implementation(Libs.grpc_android)
+
+ implementation(platform(Libs.firebase_bom))
+ implementation(Libs.firebase_analytics)
+ implementation(Libs.firebase_crashlytics)
+ implementation(Libs.firebase_messaging)
+
+ implementation(Libs.hilt_nav_compose)
+ implementation(Libs.lib_phone_number_port)
+ implementation(Libs.mp_android_chart)
+ implementation(Libs.mixpanel)
+
+ implementation(Libs.retrofit)
+ implementation(Libs.retrofit_converter)
+ implementation(Libs.okhttp_logging_interceptor)
+
+ androidTestImplementation(Libs.androidx_test_runner)
+ androidTestImplementation(Libs.androidx_junit)
+ androidTestImplementation(Libs.junit)
+ androidTestImplementation(Libs.espresso_core)
+ androidTestImplementation(Libs.espresso_contrib) {
+ exclude(module = "protobuf-lite")
+ }
+ androidTestImplementation(Libs.espresso_intents)
+ implementation(Libs.androidx_room_runtime)
+ implementation(Libs.androidx_room_ktx)
+ implementation(Libs.androidx_room_rxjava3)
+ implementation(Libs.androidx_room_paging)
+ kapt(Libs.androidx_room_compiler)
+
+ implementation(Libs.androidx_datastore)
+
+ implementation(Libs.markwon_core)
+ implementation(Libs.markwon_linkify)
+ implementation(Libs.markwon_ext_strikethrough)
+
+ implementation(Libs.play_service_auth)
+ implementation(Libs.play_service_auth_phone)
+
+ implementation(Libs.timber)
+ implementation(Libs.bugsnag)
+
+ implementation(Libs.haze)
+
+ implementation(Libs.rinku_compose)
+}
\ No newline at end of file
diff --git a/flipchatApp/proguard-rules.pro b/flipchatApp/proguard-rules.pro
new file mode 100644
index 000000000..fb05d0053
--- /dev/null
+++ b/flipchatApp/proguard-rules.pro
@@ -0,0 +1,84 @@
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-verbose
+#-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
+-obfuscationdictionary shuffled-dictionary.txt
+-classobfuscationdictionary shuffled-dictionary.txt
+
+-keepclasseswithmembernames class * {
+ native ;
+}
+#-keepclasseswithmembers class * {
+# public (android.content.Context, android.util.AttributeSet);
+#}
+
+#-keepclasseswithmembers class * {
+# public (android.content.Context, android.util.AttributeSet, int);
+#}
+
+#-keepclassmembers class * extends android.app.Activity {
+# public void *(android.view.View);
+#}
+
+#-keepclassmembers enum * {
+# public static **[] values();
+# public static ** valueOf(java.lang.String);
+#}
+
+-keepclassmembers class **.R$* {
+ public static ;
+}
+
+# Room
+-keep class * extends androidx.room.RoomDatabase
+-keep @androidx.room.Entity class *
+-keep class net.sqlcipher.** { *; }
+
+## Code API
+-keep class com.codeinc.gen.** {*;}
+-keep class com.google.protobuf.** { *; }
+
+# Keep our scan classes that interact with native
+-keep class com.kik.scan.** { *; }
+
+# BouncyCastle
+-keep public class org.bouncycastle.** # Refine this further!
+-keepclassmembers class org.bouncycastle.crypto.** {
+ ;
+}
+
+-assumenosideeffects class android.util.Log {
+ public static int v(...);
+ public static int i(...);
+ public static int w(...);
+ public static int d(...);
+ public static int e(...);
+}
+
+-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.** { *; }
+-keepclassmembers class org.json.** { *; }
+
+# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
+ -keep,allowobfuscation,allowshrinking interface retrofit2.Call
+ -keep,allowobfuscation,allowshrinking class retrofit2.Response
+
+ # With R8 full mode generic signatures are stripped for classes that are not
+ # kept. Suspend functions are wrapped in continuations where the type argument
+ # is used.
+ -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
+
+# libsodium
+-keep class com.ionspin.kotlin.crypto.** { *; }
+-keep class com.sun.jna.** { *; }
+-dontwarn java.awt.Component
+-dontwarn java.awt.GraphicsEnvironment
+-dontwarn java.awt.HeadlessException
+-dontwarn java.awt.Window
\ No newline at end of file
diff --git a/flipchatApp/shuffled-dictionary.txt b/flipchatApp/shuffled-dictionary.txt
new file mode 100644
index 000000000..1c04f878b
--- /dev/null
+++ b/flipchatApp/shuffled-dictionary.txt
@@ -0,0 +1,811 @@
+Isht
+BQUQ
+lYNE
+YWWv
+zhIz
+dRko
+ihUe
+duhS
+oRft
+jWAX
+KLsZ
+xbYj
+NxZs
+ercX
+Zikf
+jygi
+xRWw
+BBdO
+hOvC
+HEqd
+QlDS
+zchV
+mrtd
+tAMc
+beae
+NzVI
+WGdk
+mlen
+mTqY
+zOWI
+yDek
+WkdC
+EvHb
+fjdG
+tXQX
+lysK
+tJrI
+nTyR
+QoId
+teja
+tBns
+WdjJ
+rzBH
+CsrS
+rGDS
+mgig
+lKwS
+dsew
+PEIO
+Iagi
+FzdP
+jyBf
+dkuR
+gwNv
+GoKt
+bHKA
+YPEH
+sRnT
+JfEF
+ifmJ
+zyoT
+dgLf
+XeSR
+MbvH
+EviK
+etia
+MrSO
+sXtv
+Bgfm
+lRiu
+RwJl
+tUXp
+ZwyR
+daJE
+Duhs
+qXtR
+QXRy
+NKZp
+EVoJ
+APSm
+YgnC
+Xsxv
+VlNx
+fYSo
+NWos
+JpCK
+bOOW
+PpQM
+xMex
+kZmG
+fgZr
+Jxmu
+BVUp
+LBel
+WrBi
+JFtE
+jMcJ
+IiRY
+rmmT
+LyLw
+GxwL
+xwEO
+MDtQ
+tXXA
+yJqI
+XWEk
+IchN
+snux
+ycvY
+vrwB
+juTi
+wISO
+OIcx
+enkB
+ELNl
+ymTW
+ZAhU
+NZjR
+bYGt
+EHts
+lzjc
+FUZJ
+oGFN
+GRql
+kKUH
+iRfG
+kKBN
+gavV
+CtCD
+Jlcd
+rkLy
+RGmg
+sacF
+UsUf
+rJfG
+mLPF
+IsTH
+gYXw
+XNZg
+QeWt
+zONE
+osyG
+cLMf
+RhpR
+oaoi
+vaPm
+peYL
+DGbn
+IWRQ
+FOMD
+zTcd
+Hvbw
+vqPu
+lWCZ
+ZwrR
+IPnJ
+BDfW
+ZLPb
+mdFn
+ZKGS
+dOQK
+vOTL
+nGQY
+aoOn
+sJwn
+DrgD
+bIwW
+zJIB
+Fdra
+COnA
+EULK
+WlWi
+bAif
+LgwE
+yMjR
+HdjF
+vCmv
+LyGj
+lGkZ
+FIHF
+MTiY
+YHSv
+bsEG
+qluJ
+PZRG
+fXfC
+nscO
+UIxm
+rIyJ
+YUDD
+ffjW
+GXpA
+bQji
+IAyQ
+FZXu
+kclk
+ryYv
+DZOO
+rNcr
+xLYb
+BZJL
+RfqD
+DOnu
+Skiq
+VFQs
+fuzj
+cYxv
+FGTC
+VWfA
+GcgM
+EffG
+YsaB
+Kvpz
+uMqE
+iBWV
+RNlt
+NSDn
+VpKD
+IOhC
+KGja
+fznE
+LldB
+KTWJ
+SGiI
+tGCd
+mKFS
+kpjE
+inNw
+YFhv
+IoDg
+xPxa
+ZSGd
+VQAo
+WaFi
+FJOL
+bEPM
+OraL
+CosS
+Qpuw
+ugST
+yNMr
+Esbm
+FYIB
+nXLw
+IAJC
+wvIu
+rEdl
+rAIt
+RbZu
+BGAB
+JvuF
+ChCH
+ywvf
+GwoF
+lmGe
+oGcJ
+tGtZ
+wkSy
+uXTa
+OmAD
+FPNM
+yOYL
+eiJD
+GGzr
+rFrR
+Wtyr
+smhO
+Qusu
+MHBp
+gYfm
+AQMx
+JVnn
+dcdI
+cVrJ
+tTwl
+QRJf
+oyRp
+ZaOh
+Dmjk
+wDaF
+vVWP
+twIb
+FajC
+orbD
+UTlY
+yJOD
+FTaL
+tiNW
+xSNV
+lOiN
+KrqV
+oHaN
+JCWM
+EKhf
+rbdj
+dipY
+mkjj
+WELS
+FwtI
+zMLd
+PyQf
+yEtV
+xClf
+hrPJ
+Yftz
+HlbL
+cyha
+oynP
+ncsf
+atCi
+TZCr
+iwXC
+WuLG
+GdVK
+OLYf
+FgLC
+AKUb
+nKYb
+BWnv
+nkhv
+Omrp
+gsSt
+wJnx
+HZHh
+dLna
+PmVN
+juAs
+MsNP
+kjBm
+WoRy
+ujpz
+WskB
+ZxUN
+DjCe
+fyOr
+JQIG
+utyB
+ifvv
+uDEA
+usVw
+OCSt
+RCtd
+LYDI
+HejM
+SwhM
+gKcH
+Bahm
+hUTr
+SzWQ
+fxar
+PAdX
+icPy
+eCYN
+SucB
+Yjnf
+sdxY
+sogd
+wQCZ
+KbLO
+wSbt
+kLsz
+OkpF
+MPmo
+YcUL
+TPJF
+SgMm
+ekQU
+FKYn
+qZQR
+Fdtt
+XVNw
+GwFc
+XauS
+vQrD
+jmoJ
+UzaQ
+zsKx
+ndeh
+FeuU
+teGY
+Trkd
+pRLL
+zeHL
+SGPZ
+nUti
+qQws
+BLbX
+yqHy
+lmFr
+TMYU
+NlbJ
+bRAt
+bCFJ
+pAuF
+fcrw
+SVWh
+sSde
+DAGR
+AduC
+hVYM
+fUSZ
+BbRe
+RrFF
+ZKFL
+UtSD
+BVqP
+UBfb
+EgjY
+hpxM
+cyQy
+MLSV
+fpdI
+eBuR
+Kyua
+tYDV
+CNdO
+ZQUw
+yDMT
+RHAZ
+KSms
+lgaB
+bxzl
+YlIs
+MVGF
+ENts
+dgRD
+SybV
+Mpne
+DLah
+anUP
+nbFc
+KZDx
+VhDU
+UnCj
+mFPU
+QpML
+VKRz
+sMQB
+JzdA
+ZtHp
+AkzT
+LdLP
+CBrv
+CtHu
+vTfo
+uwce
+ihuY
+xqIU
+rvyG
+YIeY
+nhXV
+PvbA
+gahw
+QTPg
+Tcxn
+WDuH
+HbXF
+AJpF
+kvzs
+VhWQ
+dvhm
+CGnG
+ppzZ
+Azky
+sWsN
+UrPx
+Znvg
+ngaV
+Frhs
+lpAR
+RpIm
+eHlK
+UvCX
+jHQK
+bRCz
+Ntid
+vVcM
+wdim
+mttW
+qtHO
+hwTH
+DxKD
+shKW
+vgBc
+oWSA
+LeTC
+aweS
+tsjN
+BXOK
+vgrh
+CSvq
+rwUV
+cnhU
+VFuP
+qzBz
+Nbay
+mKib
+TQOI
+IxTd
+ufEE
+uHls
+pSTp
+JDNU
+qHMb
+wyBD
+Uxof
+hPWy
+wyZc
+arfA
+AoZs
+qpkG
+duyB
+teoC
+BHLw
+rEUj
+MMmn
+tbUY
+jcMO
+RbjW
+ofCJ
+CKNe
+YxsR
+ozgG
+UgOi
+JBJO
+PZkK
+kCIL
+JtNn
+TVAI
+zQsm
+VZgL
+rukZ
+UIEr
+Gaoa
+tFRW
+YPAz
+FoKp
+hxOK
+Zrbo
+PbMj
+mkaj
+Jfvu
+RVRh
+keIJ
+lXOG
+HkcV
+sbFt
+wFcX
+iaHo
+ncGe
+vLSU
+PvJz
+XlId
+Iqbc
+FNHp
+OauE
+Cnsp
+cYdn
+dTjy
+ARVt
+GjAP
+NeOb
+QBRs
+teHM
+NNFq
+pQtt
+Ydde
+nRZl
+jVjd
+LGOI
+NYIF
+PwqS
+Twdw
+zLme
+xomi
+PEYK
+OjFT
+JnFx
+vMSR
+MtCN
+Vmbv
+CHXq
+hVyA
+gzBT
+PmnH
+cAqk
+Fcie
+llOf
+uVTH
+goDD
+IEJJ
+kGDH
+xJPI
+LIce
+ZtnA
+NatG
+fXys
+hikO
+jeXD
+nHEB
+uVNP
+OAJq
+gdhL
+yPaj
+MyJn
+wvXg
+WuvS
+FMXw
+ZtSa
+cTtl
+ioBO
+fvdC
+ZTbi
+ZSon
+CYoZ
+pODY
+HhcT
+ABlS
+sWns
+iDwj
+cPNL
+DMIa
+Hexg
+nYen
+lPYb
+nSND
+QDYO
+gGIC
+Sgvg
+yIpe
+SiFd
+WXRO
+IxHN
+bkSq
+dlVA
+LKYw
+muTP
+HRnO
+KiMy
+uAzD
+alTU
+oEID
+EmRn
+BAzw
+LFgd
+qaUi
+crwT
+OIbV
+nnET
+pZpN
+GXiC
+aPKz
+pwdU
+ijjR
+Hohs
+DbdX
+FyRB
+fmkK
+GFkw
+EUuW
+LAFO
+TbwB
+VEmz
+Uvdu
+ypFl
+IBzE
+bzWY
+KUOp
+JFIw
+VOTC
+nIJJ
+thMd
+zCuy
+ZkaW
+vNvX
+mUfe
+iCoZ
+ZpvJ
+fbsS
+Cfzj
+orEO
+KpTn
+lzzD
+JBcp
+ISWa
+DAfh
+HlJM
+BdsO
+aytI
+LSvL
+BarO
+Wnth
+cQyc
+qwUp
+Fwgx
+QXaW
+ZiVt
+epbZ
+VVVC
+xtVZ
+lPKP
+YpCp
+wSqt
+ssNm
+wFLC
+NulV
+rsqe
+Rgde
+XKRR
+Ludw
+mYXR
+STdD
+vQPY
+OXnG
+uvkJ
+GoHg
+nROB
+duCc
+Qjrw
+WZUp
+vvVZ
+Rvtc
+VyFa
+LLdY
+qlPD
+ueda
+ClMc
+bMVB
+zmXD
+asSs
+wyaY
+bJRq
+fIsz
+yhWi
+fZXO
+qfLK
+HBIx
+ReOh
+RbVD
+Zfeo
+LNOU
+oHYO
+xOjl
+XnVx
+SwJK
+foCJ
+PxwF
+JmoH
+rqKo
+rBjl
+sNJV
+GcFn
+Moky
+WzQl
+WTzB
+AIjG
+SDfe
+dZXz
+VIDB
+Zlww
+UzuA
+nXUD
+bUrp
+QNdb
+FkSO
+imLC
+WqLj
+qSbN
+mfvq
+bPog
+uYVI
+CKbj
+BNcy
+RLng
+GHjM
+FFwD
+qSfw
+ZvlI
+JITU
+rHGC
+Wigr
+zdHB
+Orfj
+QKgP
+oVhJ
+SOml
+kEuj
+GrKj
+lVMG
+xTxC
+HCUN
+ZWTO
+vclb
+wWcc
+dckr
+vevq
+pHjd
+zBFk
+oneP
+ZhKt
+ABmP
+oJxu
+QPfa
+yjEq
+oPvN
+ZsAI
+waDy
+VKNw
+QzHV
+KPNY
+rhwm
diff --git a/flipchatApp/src/debug/AndroidManifest.xml b/flipchatApp/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..0775aa4c4
--- /dev/null
+++ b/flipchatApp/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flipchatApp/src/debug/res/values/ic_launcher_background.xml b/flipchatApp/src/debug/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..fe71b618f
--- /dev/null
+++ b/flipchatApp/src/debug/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #C372FF
+
diff --git a/flipchatApp/src/debug/res/values/strings.xml b/flipchatApp/src/debug/res/values/strings.xml
new file mode 100644
index 000000000..5587c526f
--- /dev/null
+++ b/flipchatApp/src/debug/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Flipchat Dev
+
\ No newline at end of file
diff --git a/flipchatApp/src/debug/res/xml/authenticator.xml b/flipchatApp/src/debug/res/xml/authenticator.xml
new file mode 100644
index 000000000..494bd7566
--- /dev/null
+++ b/flipchatApp/src/debug/res/xml/authenticator.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/flipchatApp/src/main/AndroidManifest.xml b/flipchatApp/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..a5b8cac79
--- /dev/null
+++ b/flipchatApp/src/main/AndroidManifest.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flipchatApp/src/main/ic_launcher-playstore.png b/flipchatApp/src/main/ic_launcher-playstore.png
new file mode 100644
index 000000000..cc79f6603
Binary files /dev/null and b/flipchatApp/src/main/ic_launcher-playstore.png differ
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt
new file mode 100644
index 000000000..cdfa71251
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt
@@ -0,0 +1,191 @@
+package xyz.flipchat.app
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.Lifecycle
+import cafe.adriel.voyager.core.registry.ScreenRegistry
+import cafe.adriel.voyager.navigator.Navigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import cafe.adriel.voyager.transitions.SlideTransition
+import com.getcode.navigation.NavScreenProvider
+import com.getcode.navigation.core.BottomSheetNavigator
+import com.getcode.navigation.core.CombinedNavigator
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.navigation.extensions.getActivityScopedViewModel
+import com.getcode.navigation.transitions.SheetSlideTransition
+import com.getcode.theme.LocalCodeColors
+import com.getcode.ui.components.OnLifecycleEvent
+import com.getcode.ui.components.bars.BottomBarContainer
+import com.getcode.ui.components.bars.TopBarContainer
+import com.getcode.ui.components.bars.rememberBarManager
+import com.getcode.ui.components.restrictions.RestrictionType
+import com.getcode.ui.decor.ScrimSupport
+import com.getcode.ui.theme.CodeScaffold
+import com.getcode.ui.utils.getActivity
+import dev.bmcreations.tipkit.TipScaffold
+import dev.bmcreations.tipkit.engines.TipsEngine
+import dev.theolm.rinku.DeepLink
+import dev.theolm.rinku.compose.ext.DeepLinkListener
+import xyz.flipchat.app.features.home.HomeViewModel
+import xyz.flipchat.app.features.payments.PaymentScaffold
+import xyz.flipchat.app.theme.FlipchatTheme
+import xyz.flipchat.app.ui.LocalUserManager
+import xyz.flipchat.app.ui.navigation.AppScreenContent
+import xyz.flipchat.app.ui.navigation.MainRoot
+import xyz.flipchat.app.util.DeeplinkType
+
+@Composable
+fun App(
+ tipsEngine: TipsEngine,
+) {
+ val homeViewModel = getActivityScopedViewModel()
+ val router = homeViewModel.router
+ val context = LocalContext.current
+
+ //We are obtaining deep link here, in case we want to allow for some amount of deep linking when not
+ //authenticated. Currently we will require authentication to see anything, but can be changed in future.
+ var deepLink by remember { mutableStateOf(null) }
+ var loginRequest by remember { mutableStateOf(null) }
+
+ DeepLinkListener {
+ val type = router.processType(it)
+ if (type is DeeplinkType.Login) {
+ loginRequest = type.entropy
+ return@DeepLinkListener
+ }
+ deepLink = it
+ }
+
+ val userManager = LocalUserManager.currentOrThrow
+ val userState by userManager.state.collectAsState()
+
+ FlipchatTheme {
+ val barManager = rememberBarManager()
+ AppScreenContent {
+ AppNavHost {
+ val codeNavigator = LocalCodeNavigator.current
+ TipScaffold(tipsEngine = tipsEngine) {
+ ScrimSupport {
+ CodeScaffold { innerPaddingModifier ->
+ PaymentScaffold {
+ Navigator(
+ screen = MainRoot { deepLink },
+ ) { navigator ->
+ LaunchedEffect(navigator.lastItem) {
+ // update global navigator for platform access to support push/pop from a single
+ // navigator current
+ codeNavigator.screensNavigator = navigator
+ }
+
+ Box(
+ modifier = Modifier
+ .padding(innerPaddingModifier)
+ ) {
+ SlideTransition(navigator)
+ }
+
+ LaunchedEffect(deepLink) {
+ if (codeNavigator.lastItem !is MainRoot) {
+ if (deepLink != null) {
+ val screenSet = router.processDestination(deepLink)
+ if (screenSet.isNotEmpty()) {
+ codeNavigator.replaceAll(screenSet)
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(loginRequest) {
+ loginRequest?.let { entropy ->
+ homeViewModel.handleLoginEntropy(
+ entropy,
+ onSwitchAccounts = {
+ loginRequest = null
+ context.getActivity()?.let {
+ homeViewModel.logout(it) {
+ codeNavigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home(entropy)))
+ }
+ }
+ },
+ onCancel = {
+ loginRequest = null
+ }
+ )
+ }
+ }
+
+ LaunchedEffect(userState.isTimelockUnlocked) {
+ if (userState.isTimelockUnlocked) {
+ codeNavigator.replaceAll(ScreenRegistry.get(NavScreenProvider.AppRestricted(RestrictionType.TIMELOCK_UNLOCKED)))
+ }
+ }
+
+ OnLifecycleEvent { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> {
+ homeViewModel.onAppOpen()
+ }
+
+ Lifecycle.Event.ON_STOP,
+ Lifecycle.Event.ON_DESTROY -> {
+ homeViewModel.closeStream()
+ }
+
+ else -> Unit
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ TopBarContainer(barManager.barMessages)
+ BottomBarContainer(barManager.barMessages)
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun AppNavHost(content: @Composable () -> Unit) {
+ var combinedNavigator by remember {
+ mutableStateOf(null)
+ }
+ BottomSheetNavigator(
+ modifier = Modifier.fillMaxSize(),
+ sheetBackgroundColor = LocalCodeColors.current.background,
+ sheetContentColor = LocalCodeColors.current.onBackground,
+ sheetContent = { sheetNav ->
+ combinedNavigator = combinedNavigator?.apply { sheetNavigator = sheetNav }
+ ?: CombinedNavigator(sheetNav)
+ combinedNavigator?.let {
+ CompositionLocalProvider(LocalCodeNavigator provides it) {
+ SheetSlideTransition(navigator = it)
+ }
+ }
+
+ },
+ onHide = com.getcode.services.manager.ModalManager::clear
+ ) { sheetNav ->
+ combinedNavigator =
+ combinedNavigator?.apply { sheetNavigator = sheetNav } ?: CombinedNavigator(sheetNav)
+ combinedNavigator?.let {
+ CompositionLocalProvider(LocalCodeNavigator provides it) {
+ content()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt
new file mode 100644
index 000000000..c4a984187
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt
@@ -0,0 +1,66 @@
+package xyz.flipchat.app
+
+import android.app.Application
+import androidx.appcompat.app.AppCompatDelegate
+import com.bugsnag.android.Bugsnag
+import com.getcode.crypt.MnemonicCache
+import com.getcode.utils.ErrorUtils
+import com.getcode.utils.trace
+import com.google.firebase.Firebase
+import com.google.firebase.crashlytics.crashlytics
+import com.google.firebase.initialize
+import dagger.hilt.android.HiltAndroidApp
+import io.reactivex.rxjava3.plugins.RxJavaPlugins
+import timber.log.Timber
+import xyz.flipchat.app.auth.AuthManager
+import javax.inject.Inject
+
+@HiltAndroidApp
+class FlipchatApp : Application() {
+
+ @Inject
+ lateinit var authManager: AuthManager
+
+ override fun onCreate() {
+ super.onCreate()
+
+ if (BuildConfig.DEBUG) {
+ Timber.plant(object : Timber.DebugTree() {
+ override fun createStackElementTag(element: StackTraceElement): String {
+ val elementTag = super.createStackElementTag(element)
+ .orEmpty()
+ .split("$")
+ .filter { it.isNotEmpty() }
+ .take(2)
+ .joinToString(" ")
+ .replace("_", " ")
+
+ val methodName = element.methodName
+ .split("$")
+ .firstOrNull()
+ .orEmpty()
+
+ return String.format(
+ "%s | %s ",
+ elementTag,
+ methodName
+ )
+ }
+ })
+ } else {
+ Bugsnag.start(this)
+ }
+
+ RxJavaPlugins.setErrorHandler {
+ ErrorUtils.handleError(it)
+ }
+
+ Firebase.initialize(this)
+ Firebase.crashlytics.setCrashlyticsCollectionEnabled(BuildConfig.NOTIFY_ERRORS || !BuildConfig.DEBUG)
+ MnemonicCache.init(this)
+ authManager.init { trace("NaCl init") }
+
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ trace("app onCreate end")
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt
new file mode 100644
index 000000000..f6808c3ff
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt
@@ -0,0 +1,130 @@
+package xyz.flipchat.app
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.os.Process.killProcess
+import android.os.Process.myPid
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.fragment.app.FragmentActivity
+import com.getcode.libs.opengraph.LocalOpenGraphParser
+import com.getcode.libs.opengraph.OpenGraphParser
+import com.getcode.network.client.Client
+import com.getcode.network.exchange.ExchangeNull
+import com.getcode.network.exchange.LocalExchange
+import xyz.flipchat.app.ui.LocalUserManager
+import com.getcode.util.resources.LocalResources
+import com.getcode.util.resources.ResourceHelper
+import com.getcode.util.vibration.LocalVibrator
+import com.getcode.util.vibration.Vibrator
+import com.getcode.utils.CurrencyUtils
+import com.getcode.utils.LocalCurrencyUtils
+import com.getcode.utils.network.LocalNetworkObserver
+import com.getcode.utils.network.NetworkConnectivityListener
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import dagger.hilt.android.AndroidEntryPoint
+import dev.bmcreations.tipkit.engines.TipsEngine
+import dev.theolm.rinku.compose.ext.Rinku
+import xyz.flipchat.app.beta.Labs
+import xyz.flipchat.app.ui.LocalLabs
+import xyz.flipchat.services.LocalPaymentController
+import xyz.flipchat.services.PaymentController
+import xyz.flipchat.services.billing.BillingClient
+import xyz.flipchat.services.billing.LocalBillingClient
+import xyz.flipchat.services.user.UserManager
+import javax.inject.Inject
+import kotlin.system.exitProcess
+
+@AndroidEntryPoint
+class MainActivity : FragmentActivity() {
+
+ @Inject
+ lateinit var resources: ResourceHelper
+
+ @Inject
+ lateinit var tipsEngine: TipsEngine
+
+ @Inject
+ lateinit var networkObserver: NetworkConnectivityListener
+
+ @Inject
+ lateinit var currencyUtils: CurrencyUtils
+
+ @Inject
+ lateinit var vibrator: Vibrator
+
+ @Inject
+ lateinit var userManager: UserManager
+
+ @Inject
+ lateinit var client: Client
+
+ @Inject
+ lateinit var paymentController: PaymentController
+
+ @Inject
+ lateinit var betaFeatures: Labs
+
+ @Inject
+ lateinit var iapController: BillingClient
+
+ @Inject
+ lateinit var openGraphParser: OpenGraphParser
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ handleUncaughtException()
+ enableEdgeToEdge()
+
+ setContent {
+ CompositionLocalProvider(
+ LocalResources provides resources,
+ LocalNetworkObserver provides networkObserver,
+ LocalExchange provides ExchangeNull(),
+ LocalCurrencyUtils provides currencyUtils,
+ LocalVibrator provides vibrator,
+ LocalUserManager provides userManager,
+ LocalPaymentController provides paymentController,
+ LocalLabs provides betaFeatures,
+ LocalBillingClient provides iapController,
+ LocalOpenGraphParser provides openGraphParser,
+ ) {
+ Rinku {
+ App(tipsEngine = tipsEngine)
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ client.startTimer()
+ iapController.connect()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ client.stopTimer()
+ }
+}
+
+private fun Activity.handleUncaughtException() {
+ val crashedKey = "isCrashed"
+ if (intent.getBooleanExtra(crashedKey, false)) return
+ Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
+ if (BuildConfig.DEBUG) throw throwable
+
+ FirebaseCrashlytics.getInstance().recordException(throwable)
+
+ val intent = Intent(this, MainActivity::class.java).apply {
+ putExtra(crashedKey, true)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ startActivity(intent)
+ finish()
+ killProcess(myPid())
+ exitProcess(2)
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt
new file mode 100644
index 000000000..9e6816b68
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt
@@ -0,0 +1,304 @@
+package xyz.flipchat.app.auth
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.core.app.NotificationManagerCompat
+import com.bugsnag.android.Bugsnag
+import com.getcode.ed25519.Ed25519
+import com.getcode.model.ID
+import xyz.flipchat.app.util.AccountUtils
+import com.getcode.services.db.Database
+import com.getcode.services.utils.token
+import com.getcode.utils.ErrorUtils
+import com.getcode.utils.TraceType
+import com.getcode.utils.base58
+import com.getcode.utils.encodeBase64
+import com.getcode.utils.trace
+import com.getcode.vendor.Base58
+import com.google.firebase.Firebase
+import com.google.firebase.messaging.FirebaseMessaging
+import com.google.firebase.messaging.messaging
+import com.ionspin.kotlin.crypto.LibsodiumInitializer
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import xyz.flipchat.FlipchatServices
+import xyz.flipchat.app.BuildConfig
+import xyz.flipchat.app.beta.Labs
+import xyz.flipchat.app.util.UserIdResult
+import xyz.flipchat.controllers.AuthController
+import xyz.flipchat.controllers.ProfileController
+import xyz.flipchat.controllers.PushController
+import xyz.flipchat.services.user.AuthState
+import xyz.flipchat.services.user.UserManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AuthManager @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val authController: AuthController,
+ private val profileController: ProfileController,
+ private val userManager: UserManager,
+ private val pushController: PushController,
+ private val betaFlags: Labs,
+ private val notificationManager: NotificationManagerCompat,
+// private val balanceController: BalanceController,
+// private val notificationCollectionHistory: NotificationCollectionHistoryController,
+// private val analytics: AnalyticsService,
+// private val mixpanelAPI: MixpanelAPI
+) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
+ private var softLoginDisabled: Boolean = false
+
+ companion object {
+ private const val TAG = "AuthManager"
+ internal fun taggedTrace(message: String, type: TraceType = TraceType.Log, cause: Throwable? = null) {
+ trace(message = message, type = type, tag = TAG, error = cause)
+ }
+ }
+
+ @SuppressLint("CheckResult")
+ fun init(onInitialized: () -> Unit = { }) {
+ launch {
+ val token = AccountUtils.getToken(context)?.token
+ softLogin(token.orEmpty())
+ .onSuccess { LibsodiumInitializer.initializeWithCallback(onInitialized) }
+ .onFailure(ErrorUtils::handleError)
+ }
+ }
+
+ private suspend fun softLogin(entropyB64: String): Result {
+ if (softLoginDisabled) return Result.failure(Throwable("Disabled"))
+ return login(entropyB64, isSoftLogin = true)
+ }
+
+ private fun setupAsNew(): String {
+ val entropyB64 = userManager.entropy
+ return if (entropyB64 == null) {
+ val seedB64 = Ed25519.createSeed16().encodeBase64()
+ userManager.establish(seedB64)
+ return seedB64
+ } else {
+ entropyB64
+ }
+ }
+
+ suspend fun createAccount(): Result {
+ val entropy = setupAsNew()
+ FlipchatServices.openDatabase(context, entropy)
+ return authController.createAccount()
+ .onSuccess { userId ->
+ AccountUtils.addAccount(
+ context = context,
+ name = "Flipchat User",
+ password = userId.base58,
+ token = entropy,
+ isUnregistered = true
+ )
+ userManager.set(userId)
+ userManager.set(AuthState.Unregistered)
+ profileController.getUserFlags()
+ }.onFailure {
+ it.printStackTrace()
+ clearToken()
+ }
+ }
+
+
+ suspend fun register(displayName: String): Result {
+ val entropyB64 = userManager.entropy ?: setupAsNew()
+ if (entropyB64.isEmpty()) {
+ taggedTrace("provided entropy was empty", type = TraceType.Error)
+ userManager.clear()
+ return Result.failure(Throwable("Provided entropy was empty"))
+ }
+
+ softLoginDisabled = true
+
+ FlipchatServices.openDatabase(context, entropyB64)
+
+ // if we are in an unregistered state attempting to register
+ // it means the user account was setup on device and a public key registered on server.
+ // in this case, we simply need to set the display name on server to flip `is_registered`.
+ if (userManager.authState is AuthState.Unregistered) {
+ userManager.set(displayName = displayName)
+
+ return profileController.setDisplayName(displayName)
+ .onSuccess {
+ AccountUtils.updateAccount(
+ context = context,
+ name = displayName,
+ )
+ userManager.set(displayName = displayName)
+ userManager.set(AuthState.LoggedIn)
+ profileController.getUserFlags()
+ savePrefs()
+ }
+ .map { userManager.userId!! }
+ } else {
+ return authController.register(displayName)
+ .onSuccess { userId ->
+ if (userManager.authState is AuthState.Unregistered) {
+ AccountUtils.updateAccount(
+ context = context,
+ name = displayName,
+ )
+ } else {
+ AccountUtils.addAccount(
+ context = context,
+ name = displayName,
+ password = userId.base58,
+ token = entropyB64,
+ isUnregistered = false,
+ )
+ userManager.set(userId = userId)
+ }
+ userManager.set(displayName = displayName)
+ userManager.set(AuthState.LoggedIn)
+ profileController.getUserFlags()
+ savePrefs()
+ }
+ .onFailure {
+ it.printStackTrace()
+ softLoginDisabled = false
+ clearToken()
+ }
+ }
+ }
+
+ suspend fun login(
+ entropyB64: String,
+ isSoftLogin: Boolean = false,
+ rollbackOnError: Boolean = false
+ ): Result {
+ taggedTrace("Login: isSoftLogin: $isSoftLogin, rollbackOnError: $rollbackOnError")
+
+ if (entropyB64.isEmpty()) {
+ taggedTrace("provided entropy was empty", type = TraceType.Error)
+ userManager.clear()
+ return Result.failure(Throwable("Provided entropy was empty"))
+ }
+
+ FlipchatServices.openDatabase(context, entropyB64)
+
+ val originalEntropy = userManager.entropy
+ userManager.establish(entropy = entropyB64)
+ userManager.set(AuthState.LoggedInAwaitingUser)
+
+ if (!isSoftLogin) {
+ loginAnalytics()
+ }
+
+ if (!isSoftLogin) softLoginDisabled = true
+
+ val lookup = AccountUtils.getUserId(context)
+
+ val ret = if (isSoftLogin) {
+ when (lookup) {
+ is UserIdResult.Registered -> Result.success(Base58.decode(lookup.userId).toList())
+ is UserIdResult.Unregistered -> Result.success(Base58.decode(lookup.userId).toList())
+ null -> Result.failure(Throwable("No user Id found"))
+ }
+ } else {
+ authController.login()
+ }
+
+ return ret
+ .map { it to profileController.getProfile(it) }
+ .map { (id, profileResult) ->
+ id to profileResult.getOrNull()?.displayName
+ }
+ .onSuccess { (userId, displayName) ->
+ if (!isSoftLogin) {
+ AccountUtils.addAccount(
+ context = context,
+ name = displayName ?: "Flipchat User",
+ password = userId.base58,
+ token = entropyB64,
+ isUnregistered = false,
+ )
+ }
+
+ userManager.set(userId = userId)
+ if (displayName != null) {
+ userManager.set(displayName = displayName)
+ }
+
+ profileController.getUserFlags()
+ .onSuccess { flags ->
+ userManager.set(flags)
+ userManager.set(authState = if (flags?.isRegistered == true) AuthState.LoggedIn else AuthState.Unregistered)
+ }.onFailure {
+ taggedTrace("Failed to get user flags", type = TraceType.Error, cause = it)
+ userManager.set(authState = AuthState.Unregistered)
+ }
+
+ savePrefs()
+ }
+ .onFailure {
+ it.printStackTrace()
+ if (rollbackOnError) {
+ login(
+ originalEntropy.orEmpty(),
+ isSoftLogin,
+ rollbackOnError = false
+ )
+ } else {
+ logout(context)
+ clearToken()
+ }
+ }.map { it.first }
+ }
+
+ suspend fun deleteAndLogout(context: Context): Result {
+ //todo: add account deletion
+ return logout(context)
+ }
+
+ suspend fun logout(context: Context): Result {
+ return AccountUtils.removeAccounts(context).toFlowable()
+ .to { runCatching { it.firstOrError().blockingGet() } }
+ .map { clearToken() }
+ .map { Result.success(Unit) }
+ }
+
+ private fun loginAnalytics() {
+ taggedTrace("analytics login event")
+// analytics.login(
+// ownerPublicKey = owner.getPublicKeyBase58(),
+// autoCompleteCount = 0,
+// inputChangeCount = 0
+// )
+ }
+
+ private suspend fun clearToken() {
+ FirebaseMessaging.getInstance().deleteToken()
+ pushController.deleteTokens()
+ notificationManager.cancelAll()
+ Database.close()
+ userManager.clear()
+ Database.delete(context)
+ betaFlags.reset()
+ if (!BuildConfig.DEBUG) Bugsnag.setUser(null, null, null)
+ }
+
+ private suspend fun savePrefs() {
+ updateFcmToken()
+ }
+
+ @SuppressLint("CheckResult")
+ private suspend fun updateFcmToken() {
+ val pushToken = Firebase.messaging.token() ?: return
+ pushController.addToken(pushToken)
+ .onSuccess {
+ trace("push token updated", type = TraceType.Silent)
+ }.onFailure {
+ trace(message = "Failure updating push token", error = it)
+ }
+ }
+
+ sealed class AuthManagerException : Exception() {
+ class TimelockUnlockedException : AuthManagerException()
+ }
+}
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt
new file mode 100644
index 000000000..77a12767e
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt
@@ -0,0 +1,28 @@
+package xyz.flipchat.app.beta
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+interface Labs {
+ fun set(flag: Lab, value: Boolean)
+ suspend fun get(flag: Lab): Boolean
+ fun observe(flag: Lab): StateFlow
+ fun observe(): StateFlow>
+ fun reset(flag: Lab)
+ fun reset()
+}
+
+object NoOpLabs: Labs {
+ override fun set(flag: Lab, value: Boolean) = Unit
+
+ override suspend fun get(flag: Lab): Boolean = false
+
+ override fun observe(flag: Lab): StateFlow = MutableStateFlow(false)
+
+ override fun observe(): StateFlow> =
+ MutableStateFlow(Lab.entries.map { BetaFeature(it, it.default) })
+
+ override fun reset(flag: Lab) = Unit
+ override fun reset() = Unit
+
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt
new file mode 100644
index 000000000..264e729af
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt
@@ -0,0 +1,169 @@
+package xyz.flipchat.app.beta
+
+import android.content.Context
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.datastore.preferences.preferencesDataStoreFile
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+sealed interface Lab {
+ val key: String
+ val default: Boolean
+ val launched: Boolean
+
+ data object ReplyToMessage : Lab {
+ override val key = "pref_reply_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object FollowerMode : Lab {
+ override val key: String = "pref_follower_mode_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object StartChatAtUnread : Lab {
+ override val key: String = "pref_start_at_unread_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object RoomNameChanges : Lab {
+ override val key: String = "pref_room_name_changes_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object DeleteMessage : Lab {
+ override val key: String = "delete_message_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object OpenCloseRoom : Lab {
+ override val key: String = "open_close_room_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object Tipping : Lab {
+ override val key: String = "tipping_enabled"
+ override val default: Boolean = true
+ override val launched: Boolean = true
+ }
+
+ data object LinkImages : Lab {
+ override val key: String = "link_image_preview_enabled"
+ override val default: Boolean = false
+ override val launched: Boolean = false
+ }
+
+ companion object {
+ val entries = listOf(
+ ReplyToMessage,
+ FollowerMode,
+ StartChatAtUnread,
+ RoomNameChanges,
+ DeleteMessage,
+ OpenCloseRoom,
+ Tipping
+ )
+
+ internal fun byKey(key: Preferences.Key<*>): Lab? {
+ return entries.firstOrNull { it.key == key.name }
+ }
+ }
+}
+
+data class BetaFeature(
+ val flag: Lab,
+ val enabled: Boolean,
+)
+
+private val Lab.preferenceKey
+ get() = booleanPreferencesKey(key)
+
+class LabsController @Inject constructor(
+ @ApplicationContext context: Context,
+) : Labs {
+ private val dataScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+ private val betaFlags = PreferenceDataStoreFactory.create(
+ corruptionHandler = ReplaceFileCorruptionHandler(
+ produceNewData = { emptyPreferences() }
+ ),
+ migrations = listOf(),
+ scope = dataScope,
+ produceFile = { context.preferencesDataStoreFile("beta-flags") }
+ )
+
+ init {
+ // reset launched flags
+ Lab.entries
+ .filter { it.launched }
+ .onEach { reset(it) }
+ }
+
+ override fun set(flag: Lab, value: Boolean) {
+ dataScope.launch(Dispatchers.IO) {
+ betaFlags.edit { prefs ->
+ prefs[flag.preferenceKey] = value
+ }
+ }
+ }
+
+ override suspend fun get(flag: Lab): Boolean {
+ return betaFlags.data.map { prefs ->
+ if (flag.launched) return@map flag.default
+ prefs[flag.preferenceKey] ?: flag.default
+ }.firstOrNull() ?: flag.default
+ }
+
+ override fun observe(flag: Lab): StateFlow = betaFlags.data.map { prefs ->
+ if (flag.launched) return@map flag.default
+ prefs[flag.preferenceKey] ?: flag.default
+ }.stateIn(dataScope, started = SharingStarted.Eagerly, flag.default)
+
+ override fun observe(): StateFlow> = betaFlags.data.map { prefs ->
+ Lab.entries.filterNot { it.launched }.map {
+ val value = if (it.launched) {
+ it.default
+ } else {
+ prefs[it.preferenceKey] ?: it.default
+ }
+
+ BetaFeature(it, value)
+ }
+ }.stateIn(
+ dataScope,
+ started = SharingStarted.Eagerly,
+ Lab.entries.map { BetaFeature(it, it.default) }
+ )
+
+ override fun reset(flag: Lab) {
+ dataScope.launch {
+ betaFlags.edit { it.remove(flag.preferenceKey) }
+ }
+ }
+
+ override fun reset() {
+ dataScope.launch {
+ betaFlags.edit { it.clear() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt
new file mode 100644
index 000000000..d7219da3b
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt
@@ -0,0 +1,7 @@
+package xyz.flipchat.app.data
+
+
+data class Account(
+ val entropy: String,
+ val userIdBase58: String
+)
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt
new file mode 100644
index 000000000..fdb3805c8
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt
@@ -0,0 +1,33 @@
+package xyz.flipchat.app.data
+
+import androidx.compose.ui.graphics.Color
+import com.getcode.model.ID
+import com.getcode.model.Kin
+import com.getcode.ui.utils.generateComplementaryColorPalette
+
+data class RoomInfo(
+ val id: ID? = null,
+ val number: Long = 0,
+ val title: String = "",
+ val imageUrl: String? = null,
+ val memberCount: Int = 0,
+ val hostId: ID? = null,
+ val hostName: String? = null,
+ val roomNumber: Long = 0,
+ val messagingFee: Kin = Kin.fromQuarks(0),
+) {
+ val customTitle: String = runCatching { Regex("^#\\d+:\\s*(.*)").find(title)?.groupValues?.get(1).orEmpty() }.getOrDefault("")
+
+ companion object {
+ val DEFAULT_GRADIENT_SAMPLE = Triple(
+ Color(0xFFFFBB00),
+ Color(0xFF7306B7),
+ Color(0xFF3E32C4),
+ )
+ }
+
+ val gradientColors: Triple
+ get() {
+ return id?.let { generateComplementaryColorPalette(it) } ?: DEFAULT_GRADIENT_SAMPLE
+ }
+}
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt
new file mode 100644
index 000000000..ccd66a499
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt
@@ -0,0 +1,291 @@
+package xyz.flipchat.app.features.accesskey
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.os.Environment
+import androidx.core.graphics.applyCanvas
+import androidx.core.graphics.drawable.toBitmap
+import androidx.lifecycle.viewModelScope
+import com.getcode.libs.qr.QRCodeGenerator
+import com.getcode.manager.TopBarManager
+import com.getcode.network.repository.DeniedReason
+import com.getcode.network.repository.ErrorSubmitIntent
+import com.getcode.network.repository.ErrorSubmitIntentException
+import com.getcode.services.manager.MnemonicManager
+import com.getcode.theme.Alert
+import com.getcode.theme.White
+import com.getcode.ui.utils.toAGColor
+import com.getcode.util.resources.ResourceHelper
+import com.getcode.utils.decodeBase64
+import com.getcode.view.BaseViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.kin.sdk.base.tools.Base58
+import timber.log.Timber
+import xyz.flipchat.app.R
+import xyz.flipchat.app.theme.FC_Primary
+import xyz.flipchat.app.util.media.MediaScanner
+import xyz.flipchat.app.util.save
+import xyz.flipchat.services.user.UserManager
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.math.roundToInt
+import com.getcode.theme.R as themeR
+
+
+data class AccessKeyUiModel(
+ val entropyB64: String? = null,
+ val isLoading: Boolean = false,
+ val isSuccess: Boolean = false,
+ val isEnabled: Boolean = true,
+ val words: List = listOf(),
+ val wordsFormatted: String = "",
+ val accessKeyBitmap: Bitmap? = null,
+ val accessKeyCroppedBitmap: Bitmap? = null,
+)
+
+abstract class BaseAccessKeyViewModel(
+ private val resources: ResourceHelper,
+ private val mnemonicManager: MnemonicManager,
+ private val mediaScanner: MediaScanner,
+ userManager: UserManager,
+ private val qrCodeGenerator: QRCodeGenerator
+) : BaseViewModel(resources) {
+ val uiFlow = MutableStateFlow(AccessKeyUiModel())
+
+ init {
+ userManager.state
+ .distinctUntilChangedBy { it.entropy }
+ .map { it.entropy }
+ .filterNotNull()
+ .take(1)
+ .onEach { initWithEntropy(it) }
+ .launchIn(viewModelScope)
+ }
+
+ fun initWithEntropy(entropyB64: String) {
+ if (uiFlow.value.entropyB64 == entropyB64) return
+ Timber.d("entropy=$entropyB64")
+ val words = mnemonicManager.fromEntropyBase64(entropyB64).words
+ val wordsFormatted = getAccessKeyText(words).joinToString("\n")
+
+ uiFlow.value = uiFlow.value.copy(
+ entropyB64 = entropyB64,
+ words = words,
+ wordsFormatted = wordsFormatted
+ )
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val accessKeyBitmap = createBitmapForExport(words = words, entropyB64 = entropyB64)
+ val accessKeyBitmapDisplay =
+ createBitmapForExport(drawBackground = false, words, entropyB64)
+ val accessKeyCroppedBitmap =
+ Bitmap.createBitmap(accessKeyBitmapDisplay, 0, 500, 1200, 1450)
+
+ uiFlow.value = uiFlow.value.copy(
+ accessKeyBitmap = accessKeyBitmap,
+ accessKeyCroppedBitmap = accessKeyCroppedBitmap
+ )
+ }
+ }
+
+ private fun getAccessKeyText(words: List): List {
+ return listOf(
+ words.subList(0, 6).joinToString(" "),
+ words.subList(6, 12).joinToString(" ")
+ )
+ }
+
+ private val targetWidth = 1200
+ private val targetHeight = 2500
+
+ private val logoWidth = 92.4f
+ private val logoHeight = 132
+ private val qrCodeSize = 360
+
+ private val bgTopOffset = 550
+ private val logoTopOffset = 770
+ private val qrTopOffset = 980
+ private val keyTextTopOffset = 1600
+ private val topTextTopOffset = 200
+ private val bottomTextTopOffset = 2000
+
+ internal suspend fun saveBitmapToFile(): Result {
+ uiFlow.update { it.copy(isLoading = true) }
+ val bitmap = uiFlow.value.accessKeyBitmap
+ ?: return Result.failure(IllegalStateException("No access key?"))
+ val destination =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+
+ return withContext(Dispatchers.IO) {
+ runCatching {
+ val result = bitmap.save(
+ destination = destination,
+ name = {
+ val date: DateFormat = SimpleDateFormat("yyy-MM-dd-h-mm", Locale.CANADA)
+ "Flipchat-Recovery-${date.format(Date())}.png"
+ }
+ )
+ if (result) {
+ mediaScanner.scan(destination)
+ }
+ result
+ }
+ }.onFailure {
+ getAccessKeySaveError()
+ uiFlow.update { it.copy(isLoading = false, isSuccess = false) }
+ }.onSuccess {
+ uiFlow.update { it.copy(isLoading = false, isSuccess = true) }
+ }
+ }
+
+ private fun createBitmapForExport(
+ drawBackground: Boolean = true,
+ words: List,
+ entropyB64: String
+ ): Bitmap {
+ val accessKeyText = getAccessKeyText(words)
+
+ val accessKeyBg = resources.getDrawable(R.drawable.ic_access_key_bg)
+ ?.toBitmap(812, 1353)!!
+
+ val imageLogo =
+ resources.getDrawable(R.drawable.ic_flipchat_logo_access_key)
+ ?.toBitmap(logoWidth.roundToInt(), logoHeight)!!
+
+ val imageOut = Bitmap.createBitmap(
+ targetWidth, targetHeight,
+ Bitmap.Config.ARGB_8888
+ ).applyCanvas {
+ val accessBgActualWidth =
+ accessKeyBg.getScaledWidth(resources.displayMetrics)
+
+ if (drawBackground) {
+ val paintBackground = Paint()
+ paintBackground.color = FC_Primary.toAGColor()
+ paintBackground.style = Paint.Style.FILL
+ drawPaint(paintBackground)
+ }
+
+ val topTextChunks = getString(R.string.subtitle_accessKeySnapshotWarning)
+ .split(" ", "\n")
+ .chunked(7)
+ .map { it.joinToString(" ") }
+
+ topTextChunks.forEachIndexed { index, text ->
+ drawText(
+ canvas = this,
+ y = topTextTopOffset + (60 * (index + 1)),
+ sizePx = 40,
+ color = Alert.toAGColor(),
+ text = text
+ )
+ }
+
+
+ drawBitmap(
+ accessKeyBg,
+ (((targetWidth - accessBgActualWidth) / 2)).toFloat(),
+ bgTopOffset.toFloat(),
+ null
+ )
+
+ drawBitmap(
+ imageLogo,
+ ((targetWidth - logoWidth) / 2).toFloat(),
+ logoTopOffset.toFloat(),
+ null
+ )
+
+ getQrCode(entropyB64)?.let { bitmap ->
+ drawBitmap(
+ bitmap,
+ ((targetWidth - qrCodeSize) / 2).toFloat(),
+ qrTopOffset.toFloat(),
+ null
+ )
+ }
+
+ drawText(
+ canvas = this,
+ y = keyTextTopOffset,
+ sizePx = 32,
+ color = White.toAGColor(),
+ text = accessKeyText[0]
+ )
+
+ drawText(
+ canvas = this,
+ y = keyTextTopOffset + 40,
+ sizePx = 32,
+ color = White.toAGColor(),
+ text = accessKeyText[1]
+ )
+
+ val bottomTextChunks = getString(R.string.subtitle_accessKeySnapshotDescription)
+ .split(" ")
+ .chunked(8)
+ .map { it.joinToString(" ") }
+
+ bottomTextChunks.forEachIndexed { index, text ->
+ drawText(
+ canvas = this,
+ y = bottomTextTopOffset + (60 * (index + 1)),
+ sizePx = 40,
+ color = White.toAGColor(),
+ text = text
+ )
+ }
+
+ }
+ return imageOut
+ }
+
+ private fun getQrCode(entropyB64: String): Bitmap? {
+ val base58 = Base58.encode(entropyB64.decodeBase64())
+ val url = "${resources.getString(R.string.app_root_url)}/login?data=$base58"
+
+ return qrCodeGenerator.generate(url, qrCodeSize)
+ }
+
+ private fun drawText(
+ canvas: Canvas,
+ y: Int,
+ x: Int? = null,
+ sizePx: Int,
+ color: Int,
+ text: String
+ ) {
+ val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ textPaint.color = color
+ textPaint.textSize = sizePx.toFloat()
+ textPaint.typeface = Typeface.create(
+ resources.getFont(themeR.font.avenir_next_demi),
+ Typeface.BOLD
+ )
+
+ val bounds1 = android.graphics.Rect()
+ textPaint.getTextBounds(text, 0, text.length, bounds1)
+ val xV: Int = x ?: ((targetWidth - bounds1.width()) / 2)
+ canvas.drawText(text, xV.toFloat(), y.toFloat(), textPaint)
+ }
+
+ private fun getAccessKeySaveError() = TopBarManager.TopBarMessage(
+ resources.getString(R.string.error_title_failedToSave),
+ resources.getString(R.string.error_description_failedToSave),
+ )
+}
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt
new file mode 100644
index 000000000..8f98f1fbe
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt
@@ -0,0 +1,341 @@
+package xyz.flipchat.app.features.balance
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Alignment.Companion.CenterVertically
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import xyz.flipchat.app.R
+import com.getcode.model.Currency
+import com.getcode.model.CurrencyCode
+import com.getcode.model.Rate
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.navigation.extensions.getActivityScopedViewModel
+import com.getcode.theme.CodeTheme
+import com.getcode.theme.DesignSystem
+import com.getcode.ui.components.text.AmountArea
+import com.getcode.ui.theme.CodeCircularProgressIndicator
+import com.getcode.utils.Kin
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import xyz.flipchat.services.user.AuthState
+
+@Parcelize
+class BalanceScreen : Screen, Parcelable {
+
+ @IgnoredOnParcel
+ override val key: ScreenKey = uniqueScreenKey
+
+ @Composable
+ override fun Content() {
+ val viewModel = getActivityScopedViewModel()
+ val state by viewModel.stateFlow.collectAsState()
+ BalanceScreenContent(state, viewModel::dispatchEvent)
+ }
+
+}
+
+@Composable
+fun BalanceScreenContent(
+ state: BalanceSheetViewModel.State,
+ dispatch: (BalanceSheetViewModel.Event) -> Unit,
+) {
+ val navigator = LocalCodeNavigator.current
+
+ BalanceContent(
+ state = state,
+ dispatch = dispatch,
+ faqOpen = { },
+// openChat = { },
+ buyMoreKin = { }
+ )
+}
+
+@Composable
+fun BalanceContent(
+ state: BalanceSheetViewModel.State,
+ dispatch: (BalanceSheetViewModel.Event) -> Unit,
+ faqOpen: () -> Unit,
+// openChat: (Chat) -> Unit,
+ buyMoreKin: () -> Unit,
+) {
+ val lazyListState = rememberLazyListState()
+ val navigator = LocalCodeNavigator.current
+
+ val chatsEmpty = false
+// val chatsEmpty by remember(state.chats) {
+// derivedStateOf { state.chats.isEmpty() }
+// }
+
+ val canClickBalance = false
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth(),
+ state = lazyListState
+ ) {
+ item {
+ Column(
+ modifier = Modifier
+ .fillParentMaxWidth()
+ .padding(horizontal = CodeTheme.dimens.inset,)
+ .padding(top = CodeTheme.dimens.inset)
+ ) {
+ BalanceTop(
+ state,
+ canClickBalance,
+ )
+ }
+ }
+
+// item {
+// Column(
+// modifier = Modifier
+// .fillParentMaxWidth()
+// .padding(horizontal = CodeTheme.dimens.inset)
+// ) {
+// if (!chatsEmpty && !state.chatsLoading && !state.isKinSelected) {
+// KinValueHint(faqOpen)
+// }
+// }
+// }
+
+// itemsIndexed(
+// state.chats,
+// key = { _, item -> item.id },
+// contentType = { _, item -> item }
+// ) { index, chat ->
+// ChatNode(chat = chat, onClick = { openChat(chat) })
+// Divider(
+// modifier = Modifier.padding(start = CodeTheme.dimens.inset),
+// color = White10,
+// )
+// }
+
+ when {
+ state.chatsLoading -> {
+ item {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(
+ CodeTheme.dimens.grid.x2,
+ CenterVertically
+ ),
+ ) {
+ CodeCircularProgressIndicator()
+ Text(
+ modifier = Modifier.fillMaxWidth(0.6f),
+ text = stringResource(R.string.subtitle_loadingBalanceAndTransactions),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+
+// chatsEmpty -> {
+// item {
+// EmptyTransactionsHint(faqOpen)
+// }
+// }
+ }
+ }
+}
+
+@Composable
+fun BalanceTop(
+ state: BalanceSheetViewModel.State,
+ isClickable: Boolean,
+ onClick: () -> Unit = {}
+) {
+ if (state.amountText.isEmpty() && state.authState is AuthState.LoggedIn) {
+
+ Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ CodeCircularProgressIndicator()
+ }
+ } else {
+ AmountArea(
+ amountText = state.amountText,
+ isAltCaption = false,
+ isAltCaptionKinIcon = false,
+ isLoading = state.chatsLoading,
+ currencyResId = state.currencyFlag,
+ isClickable = false,
+ onClick = onClick,
+ textStyle = CodeTheme.typography.displayLarge,
+ )
+ }
+}
+
+@Composable
+private fun ColumnScope.KinValueHint(onClick: () -> Unit) {
+ val context = LocalContext.current
+ Row(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ ) {
+ val annotatedBalanceString = buildAnnotatedString {
+ val infoString = stringResource(R.string.subtitle_valueKinChanges)
+ val actionString = stringResource(R.string.subtitle_learnMore)
+ val textString = "$infoString $actionString"
+
+ val startIndex = textString.indexOf(actionString)
+ val endIndex = textString.length
+ append(textString)
+
+ addStyle(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline
+ ), start = startIndex, end = endIndex
+ )
+ addStyle(
+ style = SpanStyle(color = CodeTheme.colors.textSecondary),
+ start = 0,
+ end = textString.length
+ )
+ addStringAnnotation(
+ tag = stringResource(R.string.subtitle_learnMore),
+ annotation = "",
+ start = startIndex,
+ end = endIndex
+ )
+ }
+
+ ClickableText(
+ modifier = Modifier
+ .padding(horizontal = CodeTheme.dimens.grid.x1),
+ text = annotatedBalanceString,
+ style = CodeTheme.typography.textMedium,
+ onClick = {
+ annotatedBalanceString
+ .getStringAnnotations(
+ context.getString(R.string.subtitle_learnMore),
+ it,
+ it
+ )
+ .firstOrNull()?.let { onClick() }
+ }
+ )
+ }
+}
+
+@Composable
+private fun EmptyTransactionsHint(faqOpen: () -> Unit) {
+ val context = LocalContext.current
+ Column(
+ modifier = Modifier
+ .height(200.dp)
+ .padding(horizontal = CodeTheme.dimens.grid.x6),
+ verticalArrangement = Arrangement.Bottom,
+ ) {
+ Row(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1),
+ text = stringResource(R.string.subtitle_dontHaveKin),
+ color = CodeTheme.colors.textSecondary,
+ style = CodeTheme.typography.textMedium
+ )
+ }
+
+ val annotatedLinkString: AnnotatedString = buildAnnotatedString {
+ val linkString = "Check out the FAQ"
+ val remainderString = " to find out how to get some."
+ val textString = linkString + remainderString
+
+ val startIndex = textString.indexOf(linkString)
+ val endIndex = linkString.length
+
+ append(textString)
+ addStyle(
+ style = SpanStyle(
+ color = CodeTheme.colors.textSecondary,
+ ), start = 0, end = textString.length
+ )
+ addStyle(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline
+ ), start = startIndex, end = endIndex
+ )
+
+ addStringAnnotation(
+ tag = context.getString(R.string.title_faq),
+ annotation = "",
+ start = startIndex,
+ end = endIndex
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .align(CenterHorizontally)
+ ) {
+ ClickableText(
+ text = annotatedLinkString,
+ style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center),
+ onClick = {
+ annotatedLinkString
+ .getStringAnnotations(
+ context.getString(R.string.title_faq),
+ it,
+ it
+ )
+ .firstOrNull()?.let { _ -> faqOpen() }
+ }
+ )
+ }
+ }
+}
+
+
+@Preview
+@Composable
+private fun TopPreview() {
+ DesignSystem {
+ val model = BalanceSheetViewModel.State(
+ amountText = "$12.34 of Kin",
+ marketValue = 2_225_100.0,
+ selectedRate = Rate(Currency.Kin.rate, CurrencyCode.KIN),
+ chatsLoading = false,
+ currencyFlag = R.drawable.ic_currency_kin,
+// chats = emptyList(),
+ isBucketDebuggerEnabled = false,
+ isBucketDebuggerVisible = false,
+ )
+
+ BalanceTop(
+ state = model,
+ isClickable = true
+ )
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt
new file mode 100644
index 000000000..45fe88b32
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt
@@ -0,0 +1,119 @@
+package xyz.flipchat.app.features.balance
+
+import androidx.lifecycle.viewModelScope
+import com.getcode.model.Currency
+import com.getcode.model.Rate
+import com.getcode.network.BalanceController
+import com.getcode.utils.Kin
+import com.getcode.utils.network.NetworkConnectivityListener
+import com.getcode.view.BaseViewModel2
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import xyz.flipchat.services.user.AuthState
+import xyz.flipchat.services.user.UserManager
+import javax.inject.Inject
+
+@HiltViewModel
+class BalanceSheetViewModel @Inject constructor(
+ userManager: UserManager,
+ balanceController: BalanceController,
+ networkObserver: NetworkConnectivityListener,
+) : BaseViewModel2(
+ initialState = State(),
+ updateStateForEvent = updateStateForEvent
+) {
+ data class State(
+ val authState: AuthState = AuthState.Unknown,
+ val amountText: String = "",
+ val marketValue: Double = 0.0,
+ val selectedRate: Rate? = null,
+ val isKinSelected: Boolean = false,
+ val currencyFlag: Int? = null,
+ val chatsLoading: Boolean = false,
+ val isBucketDebuggerEnabled: Boolean = false,
+ val isBucketDebuggerVisible: Boolean = false,
+ )
+
+ sealed interface Event {
+ data class OnAuthStateChanged(val authState: AuthState): Event
+ data class OnDebugBucketsEnabled(val enabled: Boolean) : Event
+ data class OnDebugBucketsVisible(val show: Boolean) : Event
+ data class OnLatestRateChanged(val rate: Rate) : Event
+
+ data class OnBalanceChanged(
+ val flagResId: Int?,
+ val marketValue: Double,
+ val display: String,
+ val isKin: Boolean,
+ ) : Event
+
+ data class OnChatsLoading(val loading: Boolean) : Event
+// data class OnChatsUpdated(val chats: List) : Event
+ data object OnOpened: Event
+ }
+
+ init {
+ userManager.state
+ .map { it.authState }
+ .onEach { dispatchEvent(Event.OnAuthStateChanged(it)) }
+ .launchIn(viewModelScope)
+
+ balanceController.formattedBalance
+ .filterNotNull()
+ .distinctUntilChanged()
+ .onEach {
+ dispatchEvent(
+ Dispatchers.Main,
+ Event.OnBalanceChanged(
+ flagResId = it.currency?.resId,
+ marketValue = it.marketValue,
+ display = it.formattedValue,
+ isKin = it.currency == Currency.Kin
+ )
+ )
+ }
+ .launchIn(viewModelScope)
+ }
+
+ companion object {
+ val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
+ when (event) {
+ is Event.OnDebugBucketsEnabled -> { state ->
+ state.copy(isBucketDebuggerEnabled = event.enabled)
+ }
+
+ is Event.OnDebugBucketsVisible -> { state ->
+ state.copy(isBucketDebuggerVisible = event.show)
+ }
+
+ is Event.OnLatestRateChanged -> { state ->
+ state.copy(selectedRate = event.rate)
+ }
+
+ is Event.OnBalanceChanged -> { state ->
+ state.copy(
+ currencyFlag = event.flagResId,
+ marketValue = event.marketValue,
+ amountText = event.display,
+ isKinSelected = event.isKin
+ )
+ }
+ is Event.OnChatsLoading -> { state ->
+ state.copy(chatsLoading = event.loading)
+ }
+// is Event.OnChatsUpdated -> { state ->
+// state.copy(chats = event.chats)
+// }
+
+ Event.OnOpened -> { state -> state }
+ is Event.OnAuthStateChanged -> { state -> state.copy(authState = event.authState) }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt
new file mode 100644
index 000000000..2c597d800
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt
@@ -0,0 +1,122 @@
+package xyz.flipchat.app.features.beta
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.theme.CodeTheme
+import com.getcode.ui.components.AppBarWithTitle
+import com.getcode.ui.components.SettingsSwitchRow
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import xyz.flipchat.app.R
+import xyz.flipchat.app.beta.Lab
+import xyz.flipchat.app.ui.LocalLabs
+
+@Parcelize
+class BetaFlagsScreen : Screen, Parcelable {
+
+ @IgnoredOnParcel
+ override val key: ScreenKey = uniqueScreenKey
+
+ @Composable
+ override fun Content() {
+ val navigator = LocalCodeNavigator.current
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AppBarWithTitle(
+ title = stringResource(R.string.title_betaFlags),
+ backButton = true,
+ onBackIconClicked = navigator::pop
+ )
+ BetaFlagsScreenContent()
+ }
+ }
+}
+
+@Composable
+private fun BetaFlagsScreenContent() {
+ val betaFlagsController = LocalLabs.current
+ val betaFlags by betaFlagsController.observe().collectAsState()
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(betaFlags) { feature ->
+ SettingsSwitchRow(
+ title = feature.flag.title,
+ subtitle = feature.flag.message,
+ checked = feature.enabled
+ ) {
+ betaFlagsController.set(feature.flag, !feature.enabled)
+ }
+ }
+ if (betaFlags.isEmpty()) {
+ item {
+ Box(modifier = Modifier.fillParentMaxSize()) {
+ Column(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "\uD83D\uDE2D",
+ style = CodeTheme.typography.displayMedium
+ )
+ Text(
+ text = "Nothing Cooking in the Lab Right Now",
+ style = CodeTheme.typography.textLarge,
+ color = CodeTheme.colors.textMain
+ )
+
+ Text(
+ text = "Check back in the next app update.",
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary
+ )
+
+ }
+ }
+ }
+ }
+ }
+}
+
+private val Lab.title: String
+ get() = when (this) {
+ Lab.FollowerMode -> "Follower Mode"
+ Lab.ReplyToMessage -> "Swipe To Reply"
+ Lab.StartChatAtUnread -> "Open Conversation @ Last Unread"
+ Lab.RoomNameChanges -> "Room Name Changes For Hosts"
+ Lab.DeleteMessage -> "Delete Message Support"
+ Lab.OpenCloseRoom -> "Open/Close Rooms"
+ Lab.Tipping -> "Tipping"
+ Lab.LinkImages -> "Show Previews for Links"
+ }
+
+private val Lab.message: String
+ get() = when (this) {
+ Lab.FollowerMode -> "When enabled, you will gain the ability to watch rooms without joining first"
+ Lab.ReplyToMessage -> "When enabled, you will gain the ability to swipe to reply to messages in chat"
+ Lab.StartChatAtUnread -> "When enabled, conversations will resume at the last message you read"
+ Lab.RoomNameChanges -> "When enabled, hosts will gain the ability to set a desired name for their room"
+ Lab.DeleteMessage -> "When enabled, hosts will gain the ability to delete messages"
+ Lab.OpenCloseRoom -> "When enabled, hosts will gain the ability to temporarily close (and reopen) their rooms"
+ Lab.Tipping -> "When enabled, you'll gain the ability to double tap messages to tip the author"
+ Lab.LinkImages -> "When enabled, links shared in chat will show a preview image for the link"
+ }
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt
new file mode 100644
index 000000000..16d62ee84
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt
@@ -0,0 +1,46 @@
+package xyz.flipchat.app.features.chat
+
+import cafe.adriel.voyager.core.registry.ScreenRegistry
+import com.getcode.manager.BottomBarManager
+import com.getcode.model.Currency
+import com.getcode.model.Kin
+import com.getcode.navigation.NavScreenProvider
+import com.getcode.navigation.core.CodeNavigator
+import com.getcode.util.resources.ResourceHelper
+import com.getcode.utils.Kin
+import com.getcode.utils.formatAmountString
+import xyz.flipchat.app.R
+import xyz.flipchat.app.features.chat.list.ChatListViewModel
+
+
+fun openChatDirectiveBottomModal(
+ resources: ResourceHelper,
+ createCost: Kin,
+ viewModel: ChatListViewModel,
+ navigator: CodeNavigator,
+) {
+ BottomBarManager.showMessage(
+ BottomBarManager.BottomBarMessage(
+ positiveText = resources.getString(R.string.action_enterRoomNumber),
+ negativeText = resources.getString(
+ R.string.action_createNewRoomWithCost,
+ formatAmountString(
+ resources = resources,
+ currency = Currency.Kin,
+ amount = createCost.quarks.toDouble(),
+ suffix = resources.getKinSuffix()
+ )
+ ),
+ negativeStyle = BottomBarManager.BottomBarButtonStyle.Filled,
+ tertiaryText = resources.getString(R.string.action_cancel),
+ onPositive = {
+ navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Lookup.Entry))
+ },
+ onNegative = {
+ viewModel.dispatchEvent(ChatListViewModel.Event.CreateRoomSelected)
+ },
+ type = BottomBarManager.BottomBarMessageType.THEMED,
+ showScrim = true,
+ )
+ )
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt
new file mode 100644
index 000000000..872fc9b1f
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt
@@ -0,0 +1,15 @@
+package xyz.flipchat.app.features.chat.conversation
+
+import com.getcode.model.Kin
+
+
+sealed interface ChattableState {
+ interface Active
+ data object DisabledByMute: ChattableState, Active
+ data class Spectator(val messageFee: Kin): ChattableState
+ data object TemporarilyEnabled: ChattableState
+ data object Enabled: ChattableState
+ data object DisabledByClosedRoom: ChattableState
+
+ fun isActiveMember() = this is Active
+}
\ No newline at end of file
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt
new file mode 100644
index 000000000..069072f78
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt
@@ -0,0 +1,227 @@
+package xyz.flipchat.app.features.chat.conversation
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.focus.FocusRequester
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.getcode.model.Currency
+import com.getcode.theme.CodeTheme
+import com.getcode.ui.components.chat.ChatInput
+import com.getcode.ui.theme.ButtonState
+import com.getcode.ui.theme.CodeButton
+import com.getcode.ui.utils.addIf
+import com.getcode.ui.utils.keyboardAsState
+import com.getcode.ui.utils.withTopBorder
+import com.getcode.util.resources.LocalResources
+import com.getcode.utils.Kin
+import com.getcode.utils.formatAmountString
+import xyz.flipchat.app.R
+
+@Composable
+fun ConversationChatInput(
+ state: ConversationViewModel.State,
+ focusRequester: FocusRequester,
+ dispatchEvent: (ConversationViewModel.Event) -> Unit,
+) {
+ var previousState by remember { mutableStateOf(null) }
+ val ime = LocalSoftwareKeyboardController.current
+ val keyboardVisible by keyboardAsState()
+
+ AnimatedContent(
+ targetState = state.chattableState,
+ transitionSpec = {
+ if (previousState == null) {
+ // Skip animation when coming from null
+ EnterTransition.None.togetherWith(ExitTransition.None)
+ } else {
+ (slideInVertically { it }).togetherWith(ExitTransition.None)
+ }
+ },
+ label = "chat input area"
+ ) { chattableState ->
+ when (chattableState) {
+ null -> {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(100.dp)
+ )
+ }
+
+ ChattableState.DisabledByMute -> {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(CodeTheme.colors.secondary)
+ .padding(
+ top = CodeTheme.dimens.grid.x1,
+ bottom = CodeTheme.dimens.grid.x3
+ )
+ .navigationBarsPadding(),
+ textAlign = TextAlign.Center,
+ text = stringResource(R.string.title_youHaveBeenMuted),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary
+ )
+ }
+
+ is ChattableState.TemporarilyEnabled,
+ is ChattableState.Enabled -> {
+ Column {
+ if (state.isHost && !keyboardVisible && state.isOpenCloseEnabled && !state.isRoomOpen) {
+ RoomOpenControlBar(
+ modifier = Modifier.fillMaxWidth(),
+ ) { dispatchEvent(ConversationViewModel.Event.OnOpenStateChangedRequested) }
+ }
+
+ ChatInput(
+ modifier = Modifier.navigationBarsPadding(),
+ state = state.textFieldState,
+ sendCashEnabled = false,
+ focusRequester = focusRequester,
+ onSendMessage = {
+ if (chattableState is ChattableState.TemporarilyEnabled) {
+ ime?.hide()
+ }
+ dispatchEvent(ConversationViewModel.Event.OnSendMessage)
+ },
+ onSendCash = { dispatchEvent(ConversationViewModel.Event.OnSendCash) }
+ )
+
+ LaunchedEffect(chattableState) {
+ if (chattableState is ChattableState.TemporarilyEnabled) {
+ focusRequester.requestFocus()
+ }
+ }
+ }
+ }
+
+ is ChattableState.Spectator -> {
+ Column(
+ modifier = Modifier
+ .addIf(
+ !state.isRoomOpen && state.isOpenCloseEnabled
+ ) {
+ Modifier.background(CodeTheme.colors.secondary)
+ }
+ .navigationBarsPadding(),
+ ) {
+ if (!state.isRoomOpen && state.isOpenCloseEnabled) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = CodeTheme.dimens.grid.x1,
+ bottom = CodeTheme.dimens.grid.x2
+ ),
+ textAlign = TextAlign.Center,
+ text = stringResource(R.string.title_roomIsClosed),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary
+ )
+ }
+
+ CodeButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = CodeTheme.dimens.inset,
+ end = CodeTheme.dimens.inset
+ ),
+ buttonState = ButtonState.Filled,
+ text = stringResource(
+ R.string.action_sendMessageInRoom,
+ formatAmountString(
+ resources = LocalResources.current!!,
+ currency = Currency.Kin,
+ amount = chattableState.messageFee.quarks.toDouble(),
+ suffix = stringResource(R.string.core_kin)
+ )
+ ),
+ ) {
+ dispatchEvent(ConversationViewModel.Event.OnSendMessageForFee)
+ }
+ }
+ }
+
+ ChattableState.DisabledByClosedRoom -> {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(CodeTheme.colors.secondary)
+ .padding(
+ top = CodeTheme.dimens.grid.x1,
+ bottom = CodeTheme.dimens.grid.x3
+ )
+ .navigationBarsPadding(),
+ textAlign = TextAlign.Center,
+ text = stringResource(R.string.title_roomIsClosed),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textSecondary
+ )
+ }
+ }
+ }
+
+ // Update the previous state
+ LaunchedEffect(state.chattableState) {
+ previousState = state.chattableState
+ }
+}
+
+@Composable
+private fun RoomOpenControlBar(
+ modifier: Modifier = Modifier,
+ onChangeRequest: () -> Unit,
+) {
+ Row(
+ modifier = modifier
+ .withTopBorder(color = CodeTheme.colors.dividerVariant)
+ .padding(CodeTheme.dimens.grid.x2),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.subtitle_roomIsClosed),
+ style = CodeTheme.typography.textSmall,
+ color = CodeTheme.colors.textMain
+ )
+
+ CodeButton(
+ text = stringResource(R.string.action_reopen),
+ shape = CircleShape,
+ buttonState = ButtonState.Filled,
+ overrideContentPadding = true,
+ contentPadding = PaddingValues(horizontal = CodeTheme.dimens.grid.x2),
+ style = CodeTheme.typography.textSmall
+ ) {
+ onChangeRequest()
+ }
+ }
+}
diff --git a/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt
new file mode 100644
index 000000000..af5517af0
--- /dev/null
+++ b/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt
@@ -0,0 +1,250 @@
+package xyz.flipchat.app.features.chat.conversation
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Surface
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.KeyboardArrowDown
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.paging.compose.LazyPagingItems
+import com.getcode.manager.TopBarManager
+import com.getcode.model.chat.AnnouncementAction
+import com.getcode.navigation.core.LocalCodeNavigator
+import com.getcode.navigation.screens.ContextSheet
+import com.getcode.theme.CodeTheme
+import com.getcode.ui.components.chat.MessageList
+import com.getcode.ui.components.chat.MessageListEvent
+import com.getcode.ui.components.chat.MessageListPointerResult
+import com.getcode.ui.components.chat.messagecontents.LocalAnnouncementActionResolver
+import com.getcode.ui.components.chat.messagecontents.ResolvedAction
+import com.getcode.ui.components.chat.utils.ChatItem
+import com.getcode.ui.components.chat.utils.HandleMessageChanges
+import com.getcode.ui.components.text.markup.Markup
+import com.getcode.ui.utils.animateScrollToItemWithFullVisibility
+import com.getcode.ui.utils.keyboardAsState
+import com.getcode.ui.utils.scrollToItemWithFullVisibility
+import com.getcode.ui.utils.verticalScrollStateGradient
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import xyz.flipchat.app.R
+import xyz.flipchat.app.util.dialNumber
+import kotlin.math.abs
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+internal fun ConversationMessages(
+ modifier: Modifier = Modifier,
+ state: ConversationViewModel.State,
+ messages: LazyPagingItems